mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-04 16:27:44 +00:00
docs(book.rs): Updated documentation for orderbook implementation
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ name = "polyfill-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Julius Tranquilli <julius@example.com>"]
|
||||
description = "Production-ready Rust client for Polymarket with HFT optimizations"
|
||||
description = "Production-ready Rust client for Polymarket"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/juliustranquilli/polyfill-rs"
|
||||
readme = "README.md"
|
||||
|
||||
+23
-122
@@ -1,129 +1,30 @@
|
||||
# Clippy configuration for polyfill-rs
|
||||
# Optimized for HFT and production code quality
|
||||
# Modern configuration format for production
|
||||
|
||||
# Performance
|
||||
unsafe_code = "forbid"
|
||||
missing_safety_doc = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
# Complexity thresholds
|
||||
cognitive-complexity-threshold = 30
|
||||
too-many-arguments-threshold = 7
|
||||
too-many-lines-threshold = 150
|
||||
type-complexity-threshold = 250
|
||||
|
||||
# Correctness
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "warn"
|
||||
unreachable = "warn"
|
||||
unimplemented = "warn"
|
||||
todo = "warn"
|
||||
# Size limits
|
||||
trivial-copy-size-limit = 128
|
||||
pass-by-value-size-limit = 256
|
||||
large-error-threshold = 128
|
||||
|
||||
# Complexity
|
||||
cognitive_complexity = "warn"
|
||||
too_many_arguments = "warn"
|
||||
too_many_lines = "warn"
|
||||
type_complexity = "warn"
|
||||
# Naming
|
||||
single-char-binding-names-threshold = 4
|
||||
enum-variant-name-threshold = 3
|
||||
|
||||
# Style
|
||||
doc_markdown = "warn"
|
||||
missing_docs = "warn"
|
||||
missing_errors_doc = "warn"
|
||||
missing_panics_doc = "warn"
|
||||
# Performance and safety
|
||||
avoid-breaking-exported-api = false
|
||||
check-private-items = true
|
||||
|
||||
# Suspicious
|
||||
assign_op_pattern = "warn"
|
||||
erasing_op = "warn"
|
||||
eval_order_dependence = "warn"
|
||||
float_cmp = "warn"
|
||||
format_push_string = "warn"
|
||||
identity_op = "warn"
|
||||
ineffective_bit_mask = "warn"
|
||||
int_plus_one = "warn"
|
||||
large_enum_variant = "warn"
|
||||
len_without_is_empty = "warn"
|
||||
let_underscore_lock = "warn"
|
||||
linkedlist = "warn"
|
||||
map_entry = "warn"
|
||||
modulo_one = "warn"
|
||||
mut_mut = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_bitwise_bool = "warn"
|
||||
needless_continue = "warn"
|
||||
needless_for_each = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_range_loop = "warn"
|
||||
needless_return = "warn"
|
||||
needless_update = "warn"
|
||||
nonminimal_bool = "warn"
|
||||
ok_expect = "warn"
|
||||
option_map_unit_fn = "warn"
|
||||
or_fun_call = "warn"
|
||||
path_buf_push_overwrite = "warn"
|
||||
precedence = "warn"
|
||||
ptr_as_ptr = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_closure = "warn"
|
||||
redundant_closure_call = "warn"
|
||||
redundant_else = "warn"
|
||||
redundant_field_names = "warn"
|
||||
redundant_guards = "warn"
|
||||
redundant_pattern = "warn"
|
||||
redundant_slicing = "warn"
|
||||
same_item_push = "warn"
|
||||
search_is_some = "warn"
|
||||
self_named_constructors = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
single_char_pattern = "warn"
|
||||
string_lit_as_bytes = "warn"
|
||||
suboptimal_flops = "warn"
|
||||
temporary_cstring_as_ptr = "warn"
|
||||
toplevel_ref_arg = "warn"
|
||||
transmute_int_to_char = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
unnecessary_filter_map = "warn"
|
||||
unnecessary_fold = "warn"
|
||||
unnecessary_mut_passed = "warn"
|
||||
unnecessary_operation = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unneeded_field_pattern = "warn"
|
||||
unreachable = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_removed_from_name = "warn"
|
||||
unused_async = "warn"
|
||||
unused_assignments = "warn"
|
||||
unused_attributes = "warn"
|
||||
unused_borrowed_ref = "warn"
|
||||
unused_collect = "warn"
|
||||
unused_comparisons = "warn"
|
||||
unused_doc_comments = "warn"
|
||||
unused_enumerate_index = "warn"
|
||||
unused_features = "warn"
|
||||
unused_imports = "warn"
|
||||
unused_labels = "warn"
|
||||
unused_macros = "warn"
|
||||
unused_parens = "warn"
|
||||
unused_qualifications = "warn"
|
||||
unused_unsafe = "warn"
|
||||
unused_variables = "warn"
|
||||
useless_attribute = "warn"
|
||||
useless_conversion = "warn"
|
||||
useless_format = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
useless_transmute = "warn"
|
||||
vec_init_then_push = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
while_let_on_iterator = "warn"
|
||||
# Documentation
|
||||
missing-docs-in-crate-items = true
|
||||
|
||||
# HFT-specific
|
||||
# Allow some performance optimizations that might be considered "unsafe" in general code
|
||||
allow = [
|
||||
"cast_possible_truncation",
|
||||
"cast_possible_wrap",
|
||||
"cast_precision_loss",
|
||||
"cast_sign_loss",
|
||||
"clippy::inline_always",
|
||||
"clippy::module_name_repetitions",
|
||||
"clippy::must_use_candidate",
|
||||
"clippy::new_without_default",
|
||||
"clippy::redundant_pub_crate",
|
||||
"clippy::too_many_arguments",
|
||||
"clippy::type_complexity",
|
||||
"clippy::upper_case_acronyms",
|
||||
"clippy::vec_init_then_push",
|
||||
]
|
||||
# Allow certain patterns common in HFT code
|
||||
allowed-idents-below-min-chars = ["id", "tx", "rx", "ok", "io", "db", "ws", "ts", "ms", "us", "ns"]
|
||||
|
||||
# Third-party crates where we allow more lenient rules
|
||||
third-party = ["serde", "tokio", "alloy"]
|
||||
@@ -0,0 +1,730 @@
|
||||
//! Comprehensive Demo for polyfill-rs
|
||||
//!
|
||||
//! This example demonstrates all the major functions and capabilities of the polyfill-rs library:
|
||||
//! - Basic client operations and API calls
|
||||
//! - Order book management and analytics
|
||||
//! - Real-time streaming capabilities
|
||||
//! - Trade execution and fill processing
|
||||
//! - Utility functions and mathematical operations
|
||||
//! - Error handling and retry logic
|
||||
//! - Rate limiting and performance optimizations
|
||||
|
||||
use polyfill_rs::{
|
||||
// Core client types
|
||||
ClobClient, PolyfillClient, OrderArgs, Side, OrderType,
|
||||
|
||||
// Order book management
|
||||
book::{OrderBook, OrderBookManager},
|
||||
|
||||
// Streaming capabilities
|
||||
stream::{WebSocketStream, StreamManager},
|
||||
|
||||
// Fill execution
|
||||
fill::{FillEngine, FillProcessor},
|
||||
|
||||
// Types and structures
|
||||
types::*,
|
||||
|
||||
// Error handling
|
||||
errors::{PolyfillError, Result},
|
||||
|
||||
// Utility functions
|
||||
utils::{crypto, math, retry, time, url, rate_limit, address},
|
||||
|
||||
// Configuration
|
||||
ClientConfig,
|
||||
};
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, info, debug};
|
||||
|
||||
/// Comprehensive demo showcasing all polyfill-rs functionality
|
||||
pub struct PolyfillDemo {
|
||||
/// Basic HTTP client
|
||||
client: ClobClient,
|
||||
/// Advanced client with configuration
|
||||
advanced_client: PolyfillClient,
|
||||
/// Order book manager
|
||||
book_manager: OrderBookManager,
|
||||
/// Fill engine for trade execution
|
||||
fill_engine: FillEngine,
|
||||
/// Fill processor for handling fills
|
||||
fill_processor: FillProcessor,
|
||||
/// Stream manager for real-time data
|
||||
stream_manager: StreamManager,
|
||||
/// Rate limiter
|
||||
rate_limiter: rate_limit::TokenBucket,
|
||||
/// Statistics
|
||||
stats: DemoStats,
|
||||
}
|
||||
|
||||
/// Demo statistics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DemoStats {
|
||||
pub api_calls: u64,
|
||||
pub orders_processed: u64,
|
||||
pub fills_processed: u64,
|
||||
pub stream_messages: u64,
|
||||
pub errors: u64,
|
||||
pub total_volume: Decimal,
|
||||
}
|
||||
|
||||
impl Default for DemoStats {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_calls: 0,
|
||||
orders_processed: 0,
|
||||
fills_processed: 0,
|
||||
stream_messages: 0,
|
||||
errors: 0,
|
||||
total_volume: dec!(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PolyfillDemo {
|
||||
/// Create a new comprehensive demo
|
||||
pub fn new() -> Result<Self> {
|
||||
// Create basic client
|
||||
let client = ClobClient::new("https://clob.polymarket.com");
|
||||
|
||||
// Create advanced client with configuration
|
||||
let _config = ClientConfig {
|
||||
base_url: "https://clob.polymarket.com".to_string(),
|
||||
chain_id: 137, // Polygon
|
||||
private_key: None, // Would be set in production
|
||||
api_credentials: None, // Would be set in production
|
||||
max_slippage: Some(dec!(0.01)), // 1% max slippage
|
||||
fee_rate: Some(dec!(0.02)), // 2% fee rate
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
max_connections: Some(100),
|
||||
};
|
||||
|
||||
let advanced_client = PolyfillClient::new("https://clob.polymarket.com");
|
||||
|
||||
// Create order book manager
|
||||
let book_manager = OrderBookManager::new(100);
|
||||
|
||||
// Create fill engine
|
||||
let fill_engine = FillEngine::new(
|
||||
dec!(1.0), // Min fill size
|
||||
dec!(2.0), // Max slippage 2%
|
||||
5, // 5 bps fee rate
|
||||
);
|
||||
|
||||
// Create fill processor
|
||||
let fill_processor = FillProcessor::new(1000);
|
||||
|
||||
// Create stream manager
|
||||
let stream_manager = StreamManager::new();
|
||||
|
||||
// Create rate limiter (100 requests per second)
|
||||
let rate_limiter = rate_limit::TokenBucket::new(100, 100);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
advanced_client,
|
||||
book_manager,
|
||||
fill_engine,
|
||||
fill_processor,
|
||||
stream_manager,
|
||||
rate_limiter,
|
||||
stats: DemoStats::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Demo 1: Basic API Operations
|
||||
pub async fn demo_basic_api_operations(&mut self) -> Result<()> {
|
||||
info!("=== Demo 1: Basic API Operations ===");
|
||||
|
||||
// Test connectivity
|
||||
let is_ok = self.client.get_ok().await;
|
||||
info!("API connectivity: {}", is_ok);
|
||||
self.stats.api_calls += 1;
|
||||
|
||||
// Get server time
|
||||
match self.client.get_server_time().await {
|
||||
Ok(timestamp) => {
|
||||
info!("Server time: {}", timestamp);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get server time: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get sampling markets
|
||||
match self.client.get_sampling_markets(Some(5)).await {
|
||||
Ok(markets) => {
|
||||
info!("Found {} markets", markets.data.len());
|
||||
for market in &markets.data[..std::cmp::min(3, markets.data.len())] {
|
||||
info!(" Market: {} - {}", market.question, market.market_slug);
|
||||
}
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get markets: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 2: Order Book Operations
|
||||
pub async fn demo_order_book_operations(&mut self) -> Result<()> {
|
||||
info!("=== Demo 2: Order Book Operations ===");
|
||||
|
||||
// Example token ID (you would use a real one in production)
|
||||
let token_id = "12345";
|
||||
|
||||
// Get order book from API
|
||||
match self.client.get_order_book(token_id).await {
|
||||
Ok(order_book) => {
|
||||
info!("Order book for token {}: {} bids, {} asks",
|
||||
token_id, order_book.bids.len(), order_book.asks.len());
|
||||
|
||||
// Create local order book
|
||||
let mut local_book = OrderBook::new(token_id.to_string(), 50);
|
||||
|
||||
// Apply order book data to local book
|
||||
for (i, bid) in order_book.bids.iter().enumerate() {
|
||||
local_book.apply_delta(OrderDelta {
|
||||
token_id: token_id.to_string(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::BUY,
|
||||
price: bid.price,
|
||||
size: bid.size,
|
||||
sequence: i as u64,
|
||||
})?;
|
||||
}
|
||||
|
||||
for (i, ask) in order_book.asks.iter().enumerate() {
|
||||
local_book.apply_delta(OrderDelta {
|
||||
token_id: token_id.to_string(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::SELL,
|
||||
price: ask.price,
|
||||
size: ask.size,
|
||||
sequence: (order_book.bids.len() + i) as u64,
|
||||
})?;
|
||||
}
|
||||
|
||||
// Get analytics
|
||||
let analytics = local_book.analytics();
|
||||
info!("Book analytics:");
|
||||
info!(" Bid levels: {}, Ask levels: {}", analytics.bid_count, analytics.ask_count);
|
||||
info!(" Total bid size: {}, Total ask size: {}", analytics.total_bid_size, analytics.total_ask_size);
|
||||
if let Some(spread) = analytics.spread {
|
||||
info!(" Spread: {} ({:.2}%)", spread, analytics.spread_pct.unwrap_or(dec!(0)));
|
||||
}
|
||||
if let Some(mid) = analytics.mid_price {
|
||||
info!(" Mid price: {}", mid);
|
||||
}
|
||||
|
||||
// Calculate market impact
|
||||
if let Some(impact) = local_book.calculate_market_impact(Side::BUY, dec!(100.0)) {
|
||||
info!("Market impact for 100 size buy:");
|
||||
info!(" Average price: {}", impact.average_price);
|
||||
info!(" Impact: {:.2}%", impact.impact_pct);
|
||||
info!(" Total cost: {}", impact.total_cost);
|
||||
}
|
||||
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get order book: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 3: Market Data Operations
|
||||
pub async fn demo_market_data_operations(&mut self) -> Result<()> {
|
||||
info!("=== Demo 3: Market Data Operations ===");
|
||||
|
||||
let token_id = "12345";
|
||||
|
||||
// Get midpoint
|
||||
match self.client.get_midpoint(token_id).await {
|
||||
Ok(midpoint) => {
|
||||
info!("Midpoint for {}: {}", token_id, midpoint.mid);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get midpoint: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get spread
|
||||
match self.client.get_spread(token_id).await {
|
||||
Ok(spread) => {
|
||||
info!("Spread for {}: {}", token_id, spread.spread);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get spread: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get price for both sides
|
||||
for side in [Side::BUY, Side::SELL] {
|
||||
match self.client.get_price(token_id, side).await {
|
||||
Ok(price) => {
|
||||
info!("{} price for {}: {}", side.as_str(), token_id, price.price);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get {} price: {}", side.as_str(), e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get tick size
|
||||
match self.client.get_tick_size(token_id).await {
|
||||
Ok(tick_size) => {
|
||||
info!("Tick size for {}: {}", token_id, tick_size);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get tick size: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get neg risk
|
||||
match self.client.get_neg_risk(token_id).await {
|
||||
Ok(neg_risk) => {
|
||||
info!("Neg risk for {}: {}", token_id, neg_risk);
|
||||
self.stats.api_calls += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get neg risk: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 4: Order Creation and Management
|
||||
pub async fn demo_order_operations(&mut self) -> Result<()> {
|
||||
info!("=== Demo 4: Order Creation and Management ===");
|
||||
|
||||
// Create order arguments
|
||||
let order_args = OrderArgs::new(
|
||||
"12345",
|
||||
dec!(0.75),
|
||||
dec!(100.0),
|
||||
Side::BUY,
|
||||
);
|
||||
|
||||
info!("Created order args: {:?}", order_args);
|
||||
|
||||
// Create market order request
|
||||
let market_order = MarketOrderRequest {
|
||||
token_id: "12345".to_string(),
|
||||
side: Side::BUY,
|
||||
amount: dec!(100.0),
|
||||
slippage_tolerance: Some(dec!(1.0)), // 1% slippage
|
||||
client_id: Some("demo_market_order".to_string()),
|
||||
};
|
||||
|
||||
info!("Created market order request: {:?}", market_order);
|
||||
|
||||
// Create limit order request
|
||||
let limit_order = OrderRequest {
|
||||
token_id: "12345".to_string(),
|
||||
side: Side::BUY,
|
||||
price: dec!(0.75),
|
||||
size: dec!(100.0),
|
||||
order_type: OrderType::GTC,
|
||||
expiration: None,
|
||||
client_id: Some("demo_limit_order".to_string()),
|
||||
};
|
||||
|
||||
info!("Created limit order request: {:?}", limit_order);
|
||||
|
||||
self.stats.orders_processed += 2;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 5: Fill Execution
|
||||
pub async fn demo_fill_execution(&mut self) -> Result<()> {
|
||||
info!("=== Demo 5: Fill Execution ===");
|
||||
|
||||
// Create a mock order book for testing
|
||||
let mut book = OrderBook::new("12345".to_string(), 50);
|
||||
|
||||
// Add some liquidity
|
||||
for i in 1..=5 {
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "12345".to_string(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::BUY,
|
||||
price: dec!(0.70) + Decimal::from(i) * dec!(0.01),
|
||||
size: dec!(100.0),
|
||||
sequence: i,
|
||||
})?;
|
||||
}
|
||||
|
||||
for i in 1..=5 {
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "12345".to_string(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::SELL,
|
||||
price: dec!(0.80) + Decimal::from(i) * dec!(0.01),
|
||||
size: dec!(100.0),
|
||||
sequence: i + 10,
|
||||
})?;
|
||||
}
|
||||
|
||||
info!("Created order book with liquidity");
|
||||
|
||||
// Execute market order
|
||||
let market_order = MarketOrderRequest {
|
||||
token_id: "12345".to_string(),
|
||||
side: Side::BUY,
|
||||
amount: dec!(50.0),
|
||||
slippage_tolerance: Some(dec!(2.0)),
|
||||
client_id: Some("demo_market_buy".to_string()),
|
||||
};
|
||||
|
||||
let fill_result = self.fill_engine.execute_market_order(&market_order, &book)?;
|
||||
|
||||
info!("Market order execution result:");
|
||||
info!(" Status: {:?}", fill_result.status);
|
||||
info!(" Total size: {}", fill_result.total_size);
|
||||
info!(" Average price: {}", fill_result.average_price);
|
||||
info!(" Total cost: {}", fill_result.total_cost);
|
||||
info!(" Fees: {}", fill_result.fees);
|
||||
info!(" Number of fills: {}", fill_result.fills.len());
|
||||
|
||||
// Process fills
|
||||
for fill in &fill_result.fills {
|
||||
self.fill_processor.process_fill(fill.clone())?;
|
||||
self.stats.fills_processed += 1;
|
||||
self.stats.total_volume += fill.size;
|
||||
}
|
||||
|
||||
// Execute limit order
|
||||
let limit_order = OrderRequest {
|
||||
token_id: "12345".to_string(),
|
||||
side: Side::SELL,
|
||||
price: dec!(0.85),
|
||||
size: dec!(25.0),
|
||||
order_type: OrderType::GTC,
|
||||
expiration: None,
|
||||
client_id: Some("demo_limit_sell".to_string()),
|
||||
};
|
||||
|
||||
let limit_result = self.fill_engine.execute_limit_order(&limit_order, &book)?;
|
||||
|
||||
info!("Limit order execution result:");
|
||||
info!(" Status: {:?}", limit_result.status);
|
||||
info!(" Total size: {}", limit_result.total_size);
|
||||
info!(" Average price: {}", limit_result.average_price);
|
||||
|
||||
self.stats.orders_processed += 2;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 6: Utility Functions
|
||||
pub async fn demo_utility_functions(&mut self) -> Result<()> {
|
||||
info!("=== Demo 6: Utility Functions ===");
|
||||
|
||||
// Time utilities
|
||||
info!("Time utilities:");
|
||||
info!(" Current timestamp (secs): {}", time::now_secs());
|
||||
info!(" Current timestamp (millis): {}", time::now_millis());
|
||||
info!(" Current timestamp (micros): {}", time::now_micros());
|
||||
|
||||
// Math utilities
|
||||
info!("Math utilities:");
|
||||
let price = dec!(0.7534);
|
||||
let tick_size = dec!(0.01);
|
||||
let rounded_price = math::round_to_tick(price, tick_size);
|
||||
info!(" Price: {}, Tick size: {}, Rounded: {}", price, tick_size, rounded_price);
|
||||
|
||||
let notional = math::notional(price, dec!(100.0));
|
||||
info!(" Notional value: {}", notional);
|
||||
|
||||
let spread_pct = math::spread_pct(dec!(0.75), dec!(0.76));
|
||||
info!(" Spread percentage: {:?}", spread_pct);
|
||||
|
||||
let mid_price = math::mid_price(dec!(0.75), dec!(0.76));
|
||||
info!(" Mid price: {:?}", mid_price);
|
||||
|
||||
// Address utilities
|
||||
info!("Address utilities:");
|
||||
let address = "0x1234567890123456789012345678901234567890";
|
||||
match address::parse_address(address) {
|
||||
Ok(addr) => info!(" Parsed address: {:?}", addr),
|
||||
Err(e) => error!(" Failed to parse address: {}", e),
|
||||
}
|
||||
|
||||
let token_id = "12345";
|
||||
match address::validate_token_id(token_id) {
|
||||
Ok(_) => info!(" Valid token ID: {}", token_id),
|
||||
Err(e) => error!(" Invalid token ID: {}", e),
|
||||
}
|
||||
|
||||
// URL utilities
|
||||
info!("URL utilities:");
|
||||
let endpoint = url::build_endpoint("https://api.example.com", "/v1/orders")?;
|
||||
info!(" Built endpoint: {}", endpoint);
|
||||
|
||||
// Rate limiting
|
||||
info!("Rate limiting:");
|
||||
for i in 0..5 {
|
||||
let allowed = self.rate_limiter.try_consume();
|
||||
info!(" Request {}: {}", i + 1, if allowed { "ALLOWED" } else { "RATE LIMITED" });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 7: Error Handling and Retry Logic
|
||||
pub async fn demo_error_handling(&mut self) -> Result<()> {
|
||||
info!("=== Demo 7: Error Handling and Retry Logic ===");
|
||||
|
||||
// Demonstrate retry logic
|
||||
let retry_config = retry::RetryConfig {
|
||||
max_attempts: 3,
|
||||
initial_delay: Duration::from_millis(100),
|
||||
max_delay: Duration::from_secs(1),
|
||||
backoff_factor: 2.0,
|
||||
jitter: true,
|
||||
};
|
||||
|
||||
let operation = || async {
|
||||
// Simulate a potentially failing operation
|
||||
if rand::random::<bool>() {
|
||||
Ok("Success!")
|
||||
} else {
|
||||
Err(PolyfillError::network("Simulated network error", std::io::Error::new(std::io::ErrorKind::Other, "Simulated error")))
|
||||
}
|
||||
};
|
||||
|
||||
match retry::with_retry(&retry_config, operation).await {
|
||||
Ok(result) => {
|
||||
info!("Retry operation succeeded: {}", result);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Retry operation failed after all attempts: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate error types
|
||||
info!("Error types demonstration:");
|
||||
|
||||
let api_error = PolyfillError::api(400, "Bad Request");
|
||||
info!(" API Error: {:?}", api_error);
|
||||
|
||||
let network_error = PolyfillError::network("Connection timeout", std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"));
|
||||
info!(" Network Error: {:?}", network_error);
|
||||
|
||||
let parse_error = PolyfillError::parse("Invalid JSON", None);
|
||||
info!(" Parse Error: {:?}", parse_error);
|
||||
|
||||
let config_error = PolyfillError::config("Invalid configuration");
|
||||
info!(" Config Error: {:?}", config_error);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 8: Streaming Capabilities (Mock)
|
||||
pub async fn demo_streaming_capabilities(&mut self) -> Result<()> {
|
||||
info!("=== Demo 8: Streaming Capabilities ===");
|
||||
|
||||
// Create a mock WebSocket stream
|
||||
let _stream = WebSocketStream::new("wss://stream.polymarket.com");
|
||||
|
||||
info!("Created WebSocket stream");
|
||||
|
||||
// Simulate subscription
|
||||
let subscription = WssSubscription {
|
||||
auth: WssAuth {
|
||||
address: "0x1234567890123456789012345678901234567890".to_string(),
|
||||
signature: "mock_signature".to_string(),
|
||||
timestamp: time::now_secs(),
|
||||
nonce: crypto::generate_nonce().to_string(),
|
||||
},
|
||||
markets: Some(vec!["market1".to_string(), "market2".to_string()]),
|
||||
asset_ids: Some(vec!["12345".to_string(), "67890".to_string()]),
|
||||
channel_type: "USER".to_string(),
|
||||
};
|
||||
|
||||
info!("Created subscription: {:?}", subscription);
|
||||
|
||||
// Simulate receiving stream messages
|
||||
let messages = vec![
|
||||
StreamMessage::Heartbeat { timestamp: chrono::Utc::now() },
|
||||
StreamMessage::BookUpdate {
|
||||
data: OrderDelta {
|
||||
token_id: "12345".to_string(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::BUY,
|
||||
price: dec!(0.75),
|
||||
size: dec!(100.0),
|
||||
sequence: 1,
|
||||
}
|
||||
},
|
||||
StreamMessage::Trade {
|
||||
data: FillEvent {
|
||||
id: "fill1".to_string(),
|
||||
order_id: "order1".to_string(),
|
||||
token_id: "12345".to_string(),
|
||||
side: Side::BUY,
|
||||
price: dec!(0.75),
|
||||
size: dec!(50.0),
|
||||
timestamp: chrono::Utc::now(),
|
||||
maker_address: alloy_primitives::Address::ZERO,
|
||||
taker_address: alloy_primitives::Address::ZERO,
|
||||
fee: dec!(0.375),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
for message in messages {
|
||||
info!("Received stream message: {:?}", message);
|
||||
self.stats.stream_messages += 1;
|
||||
|
||||
// Process message based on type
|
||||
match &message {
|
||||
StreamMessage::BookUpdate { data } => {
|
||||
info!(" Processing book update for token: {}", data.token_id);
|
||||
if let Err(e) = self.book_manager.apply_delta(data.clone()) {
|
||||
error!(" Failed to apply book update: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
StreamMessage::Trade { data } => {
|
||||
info!(" Processing trade: {} {} @ {}",
|
||||
data.side.as_str(), data.size, data.price);
|
||||
if let Err(e) = self.fill_processor.process_fill(data.clone()) {
|
||||
error!(" Failed to process fill: {}", e);
|
||||
self.stats.errors += 1;
|
||||
}
|
||||
}
|
||||
StreamMessage::Heartbeat { timestamp } => {
|
||||
debug!(" Received heartbeat at: {}", timestamp);
|
||||
}
|
||||
_ => {
|
||||
info!(" Unhandled message type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demo 9: Performance and Analytics
|
||||
pub async fn demo_performance_analytics(&mut self) -> Result<()> {
|
||||
info!("=== Demo 9: Performance and Analytics ===");
|
||||
|
||||
// Get fill engine statistics
|
||||
let fill_stats = self.fill_engine.get_stats();
|
||||
info!("Fill engine statistics:");
|
||||
info!(" Total orders: {}", fill_stats.total_orders);
|
||||
info!(" Total fills: {}", fill_stats.total_fills);
|
||||
info!(" Total volume: {}", fill_stats.total_volume);
|
||||
info!(" Total fees: {}", fill_stats.total_fees);
|
||||
|
||||
// Get fill processor statistics
|
||||
let processor_stats = self.fill_processor.get_stats();
|
||||
info!("Fill processor statistics:");
|
||||
info!(" Pending orders: {}", processor_stats.pending_orders);
|
||||
info!(" Pending fills: {}", processor_stats.pending_fills);
|
||||
info!(" Pending volume: {}", processor_stats.pending_volume);
|
||||
info!(" Processed fills: {}", processor_stats.processed_fills);
|
||||
info!(" Processed volume: {}", processor_stats.processed_volume);
|
||||
|
||||
// Get demo statistics
|
||||
info!("Demo statistics:");
|
||||
info!(" API calls: {}", self.stats.api_calls);
|
||||
info!(" Orders processed: {}", self.stats.orders_processed);
|
||||
info!(" Fills processed: {}", self.stats.fills_processed);
|
||||
info!(" Stream messages: {}", self.stats.stream_messages);
|
||||
info!(" Errors: {}", self.stats.errors);
|
||||
info!(" Total volume: {}", self.stats.total_volume);
|
||||
|
||||
// Calculate error rate
|
||||
let total_operations = self.stats.api_calls + self.stats.orders_processed + self.stats.stream_messages;
|
||||
let error_rate = if total_operations > 0 {
|
||||
(self.stats.errors as f64 / total_operations as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
info!(" Error rate: {:.2}%", error_rate);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run all demos
|
||||
pub async fn run_all_demos(&mut self) -> Result<()> {
|
||||
info!("Starting comprehensive polyfill-rs demo...");
|
||||
|
||||
// Run all demo sections
|
||||
self.demo_basic_api_operations().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_order_book_operations().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_market_data_operations().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_order_operations().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_fill_execution().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_utility_functions().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_error_handling().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_streaming_capabilities().await?;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
self.demo_performance_analytics().await?;
|
||||
|
||||
info!("Comprehensive demo completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
info!("Polyfill-rs Comprehensive Demo");
|
||||
info!("==============================");
|
||||
|
||||
// Create and run demo
|
||||
let mut demo = PolyfillDemo::new()?;
|
||||
|
||||
if let Err(e) = demo.run_all_demos().await {
|
||||
error!("Demo failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
info!("Demo completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
+5
-5
@@ -205,9 +205,9 @@ impl SnipeStrategy {
|
||||
|
||||
// Determine side based on market conditions
|
||||
let side = if bid > ask {
|
||||
Side::Sell // Crossed market, sell
|
||||
Side::SELL // Crossed market, sell
|
||||
} else {
|
||||
Side::Buy // Normal market, buy
|
||||
Side::BUY // Normal market, buy
|
||||
};
|
||||
|
||||
// Create market order request
|
||||
@@ -228,7 +228,7 @@ impl SnipeStrategy {
|
||||
book_impl.apply_delta(OrderDelta {
|
||||
token_id: self.token_id.clone(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::Buy,
|
||||
side: Side::BUY,
|
||||
price: level.price,
|
||||
size: level.size,
|
||||
sequence: 1,
|
||||
@@ -239,7 +239,7 @@ impl SnipeStrategy {
|
||||
book_impl.apply_delta(OrderDelta {
|
||||
token_id: self.token_id.clone(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
side: Side::Sell,
|
||||
side: Side::SELL,
|
||||
price: level.price,
|
||||
size: level.size,
|
||||
sequence: 2,
|
||||
@@ -327,7 +327,7 @@ impl MockMarketData {
|
||||
let new_price = self.base_price * (Decimal::from(1) + price_change);
|
||||
|
||||
// Generate order book update
|
||||
let side = if rand::random::<bool>() { Side::Buy } else { Side::Sell };
|
||||
let side = if rand::random::<bool>() { Side::BUY } else { Side::SELL };
|
||||
let size = Decimal::from(rand::random::<u64>() % 1000 + 100);
|
||||
|
||||
StreamMessage::BookUpdate {
|
||||
|
||||
+184
-84
@@ -1,75 +1,101 @@
|
||||
//! Order book management for Polymarket client
|
||||
//!
|
||||
//! This module provides high-performance order book operations optimized
|
||||
//! for latency-sensitive trading environments.
|
||||
|
||||
use crate::errors::{PolyfillError, Result};
|
||||
use crate::types::*;
|
||||
use crate::utils::math;
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tracing::{debug, trace, warn};
|
||||
use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically - crucial for order books
|
||||
use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks
|
||||
use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
|
||||
use chrono::Utc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// High-performance order book implementation
|
||||
///
|
||||
/// This is the core data structure that holds all the live buy/sell orders for a token.
|
||||
/// The efficiency of this code is critical as the order book is constantly being updated as orders are added and removed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OrderBook {
|
||||
/// Token ID this book represents
|
||||
/// Token ID this book represents (like "123456" for a specific prediction market outcome)
|
||||
pub token_id: String,
|
||||
|
||||
/// Current sequence number for ordering updates
|
||||
/// This helps us ignore old/duplicate updates that arrive out of order
|
||||
pub sequence: u64,
|
||||
/// Last update timestamp
|
||||
|
||||
/// Last update timestamp - when we last got new data for this book
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
|
||||
/// Bid side (price -> size, sorted descending)
|
||||
/// BTreeMap automatically keeps highest bids first, which is what we want
|
||||
/// Key = price (like 0.65), Value = total size at that price (like 1000 tokens)
|
||||
bids: BTreeMap<Decimal, Decimal>,
|
||||
/// Ask side (price -> size, sorted ascending)
|
||||
|
||||
/// Ask side (price -> size, sorted ascending)
|
||||
/// BTreeMap keeps lowest asks first - people selling at cheapest prices
|
||||
asks: BTreeMap<Decimal, Decimal>,
|
||||
/// Minimum tick size for this market
|
||||
|
||||
/// Minimum tick size for this market (like 0.01 = prices must be in penny increments)
|
||||
/// Some markets only allow certain price increments
|
||||
tick_size: Option<Decimal>,
|
||||
/// Maximum depth to maintain
|
||||
|
||||
/// Maximum depth to maintain (how many price levels to keep)
|
||||
///
|
||||
/// We don't need to track every single price level, just the best ones because:
|
||||
/// - Trading reality 90% of volume happens in the top 5-10 price levels
|
||||
/// - Execution priority: Orders get filled from best price first, so deep levels often don't matter
|
||||
/// - Market efficiency: If you're buying and best ask is $0.67, you'll never pay $0.95
|
||||
/// - Risk management: Large orders that would hit deep levels are usually broken up
|
||||
/// - Data freshness: Deep levels often have stale orders from hours/days ago
|
||||
///
|
||||
/// Typical values: 10-50 for retail, 100-500 for institutional HFT systems
|
||||
max_depth: usize,
|
||||
}
|
||||
|
||||
impl OrderBook {
|
||||
/// Create a new order book
|
||||
/// Just sets up empty bid/ask maps and basic metadata
|
||||
pub fn new(token_id: String, max_depth: usize) -> Self {
|
||||
Self {
|
||||
token_id,
|
||||
sequence: 0,
|
||||
sequence: 0, // Start at 0, will increment as we get updates
|
||||
timestamp: Utc::now(),
|
||||
bids: BTreeMap::new(),
|
||||
asks: BTreeMap::new(),
|
||||
tick_size: None,
|
||||
bids: BTreeMap::new(), // Empty to start
|
||||
asks: BTreeMap::new(), // Empty to start
|
||||
tick_size: None, // We'll set this later when we learn about the market
|
||||
max_depth,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the tick size for this book
|
||||
/// This tells us the minimum price increment allowed (like 0.01 for penny increments)
|
||||
pub fn set_tick_size(&mut self, tick_size: Decimal) {
|
||||
self.tick_size = Some(tick_size);
|
||||
}
|
||||
|
||||
/// Get the current best bid
|
||||
/// Get the current best bid (highest price someone is willing to pay)
|
||||
/// Uses next_back() because BTreeMap sorts ascending, but we want the highest bid
|
||||
pub fn best_bid(&self) -> Option<BookLevel> {
|
||||
self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size })
|
||||
}
|
||||
|
||||
/// Get the current best ask
|
||||
/// Get the current best ask (lowest price someone is willing to sell at)
|
||||
/// Uses next() because BTreeMap sorts ascending, so first item is lowest ask
|
||||
pub fn best_ask(&self) -> Option<BookLevel> {
|
||||
self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
|
||||
}
|
||||
|
||||
/// Get the current spread
|
||||
/// Get the current spread (difference between best ask and best bid)
|
||||
/// This tells us how "tight" the market is - smaller spread = more liquid market
|
||||
pub fn spread(&self) -> Option<Decimal> {
|
||||
match (self.best_bid(), self.best_ask()) {
|
||||
(Some(bid), Some(ask)) => Some(ask.price - bid.price),
|
||||
_ => None,
|
||||
_ => None, // Can't calculate spread if we're missing bid or ask
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current mid price
|
||||
/// Get the current mid price (halfway between best bid and ask)
|
||||
/// This is often used as the "fair value" of the market
|
||||
pub fn mid_price(&self) -> Option<Decimal> {
|
||||
math::mid_price(
|
||||
self.best_bid()?.price,
|
||||
@@ -77,7 +103,8 @@ impl OrderBook {
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the spread as a percentage
|
||||
/// Get the spread as a percentage (relative to the bid price)
|
||||
/// Useful for comparing spreads across different price levels
|
||||
pub fn spread_pct(&self) -> Option<Decimal> {
|
||||
match (self.best_bid(), self.best_ask()) {
|
||||
(Some(bid), Some(ask)) => math::spread_pct(bid.price, ask.price),
|
||||
@@ -85,57 +112,63 @@ impl OrderBook {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all bids up to a certain depth
|
||||
/// Get all bids up to a certain depth (top N price levels)
|
||||
/// Returns them in descending price order (best bids first)
|
||||
pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
|
||||
let depth = depth.unwrap_or(self.max_depth);
|
||||
self.bids
|
||||
.iter()
|
||||
.rev()
|
||||
.take(depth)
|
||||
.rev() // Reverse because we want highest prices first
|
||||
.take(depth) // Only take the top N levels
|
||||
.map(|(&price, &size)| BookLevel { price, size })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all asks up to a certain depth
|
||||
/// Get all asks up to a certain depth (top N price levels)
|
||||
/// Returns them in ascending price order (best asks first)
|
||||
pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
|
||||
let depth = depth.unwrap_or(self.max_depth);
|
||||
self.asks
|
||||
.iter()
|
||||
.take(depth)
|
||||
.iter() // Already in ascending order, so no need to reverse
|
||||
.take(depth) // Only take the top N levels
|
||||
.map(|(&price, &size)| BookLevel { price, size })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the full book snapshot
|
||||
/// Creates a copy of the current state that can be safely passed around
|
||||
/// without worrying about the original book changing
|
||||
pub fn snapshot(&self) -> crate::types::OrderBook {
|
||||
crate::types::OrderBook {
|
||||
token_id: self.token_id.clone(),
|
||||
timestamp: self.timestamp,
|
||||
bids: self.bids(None),
|
||||
asks: self.asks(None),
|
||||
bids: self.bids(None), // Get all bids (up to max_depth)
|
||||
asks: self.asks(None), // Get all asks (up to max_depth)
|
||||
sequence: self.sequence,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a delta update to the book
|
||||
/// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70"
|
||||
pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
|
||||
// Validate sequence ordering
|
||||
// Validate sequence ordering - ignore old updates that arrive late
|
||||
// This is crucial for maintaining data integrity in real-time systems
|
||||
if delta.sequence <= self.sequence {
|
||||
trace!("Ignoring stale delta: {} <= {}", delta.sequence, self.sequence);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Update sequence and timestamp
|
||||
// Update our tracking info
|
||||
self.sequence = delta.sequence;
|
||||
self.timestamp = delta.timestamp;
|
||||
|
||||
// Apply the delta
|
||||
// Apply the actual change to the appropriate side
|
||||
match delta.side {
|
||||
Side::BUY => self.apply_bid_delta(delta.price, delta.size),
|
||||
Side::SELL => self.apply_ask_delta(delta.price, delta.size),
|
||||
}
|
||||
|
||||
// Maintain depth limits
|
||||
// Keep the book from getting too deep (memory management)
|
||||
self.trim_depth();
|
||||
|
||||
debug!(
|
||||
@@ -149,82 +182,114 @@ impl OrderBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a bid-side delta
|
||||
/// Apply a bid-side delta (someone wants to buy)
|
||||
/// If size is 0, it means "remove this price level entirely"
|
||||
/// Otherwise, set the total size at this price level
|
||||
fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
|
||||
if size.is_zero() {
|
||||
self.bids.remove(&price);
|
||||
self.bids.remove(&price); // No more buyers at this price
|
||||
} else {
|
||||
self.bids.insert(price, size);
|
||||
self.bids.insert(price, size); // Update total size at this price
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply an ask-side delta
|
||||
/// Apply an ask-side delta (someone wants to sell)
|
||||
/// Same logic as bids - size of 0 means remove the price level
|
||||
fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
|
||||
if size.is_zero() {
|
||||
self.asks.remove(&price);
|
||||
self.asks.remove(&price); // No more sellers at this price
|
||||
} else {
|
||||
self.asks.insert(price, size);
|
||||
self.asks.insert(price, size); // Update total size at this price
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim the book to maintain depth limits
|
||||
/// We don't want to track every single price level - just the best ones
|
||||
///
|
||||
/// Why limit depth? Several reasons:
|
||||
/// 1. Memory efficiency: A popular market might have thousands of price levels,
|
||||
/// but only the top 10-50 levels are actually tradeable with reasonable size
|
||||
/// 2. Performance: Fewer levels = faster iteration when calculating market impact
|
||||
/// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
|
||||
/// mostly noise and will never get hit in normal trading
|
||||
/// 4. Stale data: Deep levels often contain old orders that haven't been cancelled
|
||||
/// 5. Network bandwidth: Less data to send when streaming updates
|
||||
|
||||
fn trim_depth(&mut self) {
|
||||
// For bids, remove the LOWEST prices (worst bids) if we have too many
|
||||
// Example: If best bid is $0.65, we don't care about bids at $0.10
|
||||
if self.bids.len() > self.max_depth {
|
||||
let to_remove = self.bids.len() - self.max_depth;
|
||||
for _ in 0..to_remove {
|
||||
self.bids.pop_first();
|
||||
self.bids.pop_first(); // Remove lowest bid prices (furthest from market)
|
||||
}
|
||||
}
|
||||
|
||||
// For asks, remove the HIGHEST prices (worst asks) if we have too many
|
||||
// Example: If best ask is $0.67, we don't care about asks at $0.95
|
||||
if self.asks.len() > self.max_depth {
|
||||
let to_remove = self.asks.len() - self.max_depth;
|
||||
for _ in 0..to_remove {
|
||||
self.asks.pop_last();
|
||||
self.asks.pop_last(); // Remove highest ask prices (furthest from market)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the market impact for a given order size
|
||||
/// This is exactly why we don't need deep levels - if your order would require
|
||||
/// hitting prices way off the current market (like $0.95 when best ask is $0.67),
|
||||
/// you'd never actually place that order. You'd either:
|
||||
/// 1. Break it into smaller pieces over time
|
||||
/// 2. Use a different trading strategy
|
||||
/// 3. Accept that there's not enough liquidity right now
|
||||
pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
|
||||
// Get the levels we'd be trading against
|
||||
let levels = match side {
|
||||
Side::BUY => self.asks(None),
|
||||
Side::SELL => self.bids(None),
|
||||
Side::BUY => self.asks(None), // If buying, we hit the ask side
|
||||
Side::SELL => self.bids(None), // If selling, we hit the bid side
|
||||
};
|
||||
|
||||
if levels.is_empty() {
|
||||
return None;
|
||||
return None; // No liquidity available
|
||||
}
|
||||
|
||||
let mut remaining_size = size;
|
||||
let mut total_cost = Decimal::ZERO;
|
||||
let mut weighted_price = Decimal::ZERO;
|
||||
|
||||
// Walk through each price level, filling as much as we can
|
||||
for level in levels {
|
||||
let fill_size = std::cmp::min(remaining_size, level.size);
|
||||
let level_cost = fill_size * level.price;
|
||||
|
||||
total_cost += level_cost;
|
||||
weighted_price += level_cost;
|
||||
weighted_price += level_cost; // This accumulates the weighted average
|
||||
remaining_size -= fill_size;
|
||||
|
||||
if remaining_size.is_zero() {
|
||||
break;
|
||||
break; // We've filled our entire order
|
||||
}
|
||||
}
|
||||
|
||||
if remaining_size > Decimal::ZERO {
|
||||
return None; // Not enough liquidity
|
||||
// Not enough liquidity to fill the whole order
|
||||
// This is a perfect example of why we don't need infinite depth:
|
||||
// If we can't fill your order with the top N levels, you probably
|
||||
// shouldn't be placing that order anyway - it would move the market too much
|
||||
return None;
|
||||
}
|
||||
|
||||
let avg_price = weighted_price / size;
|
||||
|
||||
// Calculate how much we moved the market compared to the best price
|
||||
let impact = match side {
|
||||
Side::BUY => {
|
||||
let best_ask = self.best_ask()?.price;
|
||||
(avg_price - best_ask) / best_ask
|
||||
(avg_price - best_ask) / best_ask // How much worse than best ask
|
||||
}
|
||||
Side::SELL => {
|
||||
let best_bid = self.best_bid()?.price;
|
||||
(best_bid - avg_price) / best_bid
|
||||
(best_bid - avg_price) / best_bid // How much worse than best bid
|
||||
}
|
||||
};
|
||||
|
||||
@@ -237,20 +302,23 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
/// Check if the book is stale (no recent updates)
|
||||
/// Useful for detecting when we've lost connection to live data
|
||||
pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
|
||||
let age = Utc::now() - self.timestamp;
|
||||
age > chrono::Duration::from_std(max_age).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the total liquidity at a given price level
|
||||
/// Tells you how much you can buy/sell at exactly this price
|
||||
pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
|
||||
match side {
|
||||
Side::BUY => self.asks.get(&price).copied().unwrap_or_default(),
|
||||
Side::SELL => self.bids.get(&price).copied().unwrap_or_default(),
|
||||
Side::BUY => self.asks.get(&price).copied().unwrap_or_default(), // How much we can buy at this price
|
||||
Side::SELL => self.bids.get(&price).copied().unwrap_or_default(), // How much we can sell at this price
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total liquidity within a price range
|
||||
/// Useful for understanding how much depth exists in a certain price band
|
||||
pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal {
|
||||
let levels: Vec<_> = match side {
|
||||
Side::BUY => self.asks.range(min_price..=max_price).collect(),
|
||||
@@ -261,32 +329,44 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
/// Validate that prices are properly ordered
|
||||
/// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match (self.best_bid(), self.best_ask()) {
|
||||
(Some(bid), Some(ask)) => bid.price < ask.price,
|
||||
_ => true, // Empty book is valid
|
||||
(Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition
|
||||
_ => true, // Empty book is technically valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Market impact calculation result
|
||||
/// This tells you what would happen if you executed a large order
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketImpact {
|
||||
pub average_price: Decimal,
|
||||
pub impact_pct: Decimal,
|
||||
pub total_cost: Decimal,
|
||||
pub size_filled: Decimal,
|
||||
pub average_price: Decimal, // The average price you'd get across all fills
|
||||
pub impact_pct: Decimal, // How much worse than the best price (as percentage)
|
||||
pub total_cost: Decimal, // Total amount you'd pay/receive
|
||||
pub size_filled: Decimal, // How much of your order got filled
|
||||
}
|
||||
|
||||
/// Thread-safe order book manager
|
||||
/// This manages multiple order books (one per token) and handles concurrent access
|
||||
/// Multiple threads can read/write different books simultaneously
|
||||
///
|
||||
/// The depth limiting becomes even more critical here because we might be tracking
|
||||
/// hundreds or thousands of different tokens simultaneously. If each book had
|
||||
/// unlimited depth, we could easily use gigabytes of RAM for mostly useless data.
|
||||
///
|
||||
/// Example: 1000 tokens × 1000 price levels × 32 bytes per level = 32MB just for prices
|
||||
/// With depth limiting: 1000 tokens × 50 levels × 32 bytes = 1.6MB (20x less memory)
|
||||
#[derive(Debug)]
|
||||
pub struct OrderBookManager {
|
||||
books: Arc<RwLock<std::collections::HashMap<String, OrderBook>>>,
|
||||
books: Arc<RwLock<std::collections::HashMap<String, OrderBook>>>, // Token ID -> OrderBook
|
||||
max_depth: usize,
|
||||
}
|
||||
|
||||
impl OrderBookManager {
|
||||
/// Create a new order book manager
|
||||
/// Starts with an empty collection of books
|
||||
pub fn new(max_depth: usize) -> Self {
|
||||
Self {
|
||||
books: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
@@ -295,14 +375,16 @@ impl OrderBookManager {
|
||||
}
|
||||
|
||||
/// Get or create an order book for a token
|
||||
/// If we don't have a book for this token yet, create a new empty one
|
||||
pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
|
||||
if let Some(book) = books.get(token_id) {
|
||||
Ok(book.clone())
|
||||
Ok(book.clone()) // Return a copy of the existing book
|
||||
} else {
|
||||
// Create a new book for this token
|
||||
let book = OrderBook::new(token_id.to_string(), self.max_depth);
|
||||
books.insert(token_id.to_string(), book.clone());
|
||||
Ok(book)
|
||||
@@ -310,11 +392,13 @@ impl OrderBookManager {
|
||||
}
|
||||
|
||||
/// Update a book with a delta
|
||||
/// This is called when we receive real-time updates from the exchange
|
||||
pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
|
||||
// Find the book for this token (must already exist)
|
||||
let book = books
|
||||
.get_mut(&delta.token_id)
|
||||
.ok_or_else(|| {
|
||||
@@ -324,10 +408,12 @@ impl OrderBookManager {
|
||||
)
|
||||
})?;
|
||||
|
||||
// Apply the update to the specific book
|
||||
book.apply_delta(delta)
|
||||
}
|
||||
|
||||
/// Get a book snapshot
|
||||
/// Returns a copy of the current book state that won't change
|
||||
pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
|
||||
let books = self.books.read().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
@@ -335,7 +421,7 @@ impl OrderBookManager {
|
||||
|
||||
books
|
||||
.get(token_id)
|
||||
.map(|book| book.snapshot())
|
||||
.map(|book| book.snapshot()) // Create a snapshot copy
|
||||
.ok_or_else(|| {
|
||||
PolyfillError::market_data(
|
||||
format!("No book found for token: {}", token_id),
|
||||
@@ -345,6 +431,7 @@ impl OrderBookManager {
|
||||
}
|
||||
|
||||
/// Get all available books
|
||||
/// Returns snapshots of every book we're currently tracking
|
||||
pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
|
||||
let books = self.books.read().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
@@ -354,13 +441,15 @@ impl OrderBookManager {
|
||||
}
|
||||
|
||||
/// Remove stale books
|
||||
/// Cleans up books that haven't been updated recently (probably disconnected)
|
||||
/// This prevents memory leaks from accumulating dead books
|
||||
pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
|
||||
let initial_count = books.len();
|
||||
books.retain(|_, book| !book.is_stale(max_age));
|
||||
books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books
|
||||
let removed = initial_count - books.len();
|
||||
|
||||
if removed > 0 {
|
||||
@@ -372,27 +461,29 @@ impl OrderBookManager {
|
||||
}
|
||||
|
||||
/// Order book analytics and statistics
|
||||
/// Provides a summary view of the book's health and characteristics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookAnalytics {
|
||||
pub token_id: String,
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
pub bid_count: usize,
|
||||
pub ask_count: usize,
|
||||
pub total_bid_size: Decimal,
|
||||
pub total_ask_size: Decimal,
|
||||
pub spread: Option<Decimal>,
|
||||
pub spread_pct: Option<Decimal>,
|
||||
pub mid_price: Option<Decimal>,
|
||||
pub volatility: Option<Decimal>,
|
||||
pub bid_count: usize, // How many different bid price levels
|
||||
pub ask_count: usize, // How many different ask price levels
|
||||
pub total_bid_size: Decimal, // Total size of all bids combined
|
||||
pub total_ask_size: Decimal, // Total size of all asks combined
|
||||
pub spread: Option<Decimal>, // Current spread (ask - bid)
|
||||
pub spread_pct: Option<Decimal>, // Spread as percentage
|
||||
pub mid_price: Option<Decimal>, // Current mid price
|
||||
pub volatility: Option<Decimal>, // Price volatility (if calculated)
|
||||
}
|
||||
|
||||
impl OrderBook {
|
||||
/// Calculate analytics for this book
|
||||
/// Gives you a quick health check of the market
|
||||
pub fn analytics(&self) -> BookAnalytics {
|
||||
let bid_count = self.bids.len();
|
||||
let ask_count = self.asks.len();
|
||||
let total_bid_size: Decimal = self.bids.values().sum();
|
||||
let total_ask_size: Decimal = self.asks.values().sum();
|
||||
let total_bid_size: Decimal = self.bids.values().sum(); // Add up all bid sizes
|
||||
let total_ask_size: Decimal = self.asks.values().sum(); // Add up all ask sizes
|
||||
|
||||
BookAnalytics {
|
||||
token_id: self.token_id.clone(),
|
||||
@@ -409,9 +500,11 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
/// Calculate price volatility (simplified)
|
||||
/// This is a placeholder - real volatility needs historical price data
|
||||
fn calculate_volatility(&self) -> Option<Decimal> {
|
||||
// This is a simplified volatility calculation
|
||||
// In a real implementation, you'd want to track price history
|
||||
// In a real implementation, you'd want to track price history over time
|
||||
// and calculate standard deviation of price changes
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -419,20 +512,23 @@ impl OrderBook {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rust_decimal_macros::dec;
|
||||
use rust_decimal_macros::dec; // Convenient macro for creating Decimal literals
|
||||
|
||||
#[test]
|
||||
fn test_order_book_creation() {
|
||||
// Test that we can create a new empty order book
|
||||
let book = OrderBook::new("test_token".to_string(), 10);
|
||||
assert_eq!(book.token_id, "test_token");
|
||||
assert_eq!(book.bids.len(), 0);
|
||||
assert_eq!(book.asks.len(), 0);
|
||||
assert_eq!(book.bids.len(), 0); // Should start empty
|
||||
assert_eq!(book.asks.len(), 0); // Should start empty
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_delta() {
|
||||
// Test that we can apply order book updates
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
// Create a buy order at $0.50 for 100 tokens
|
||||
let delta = OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
@@ -443,16 +539,17 @@ mod tests {
|
||||
};
|
||||
|
||||
book.apply_delta(delta).unwrap();
|
||||
assert_eq!(book.sequence, 1);
|
||||
assert_eq!(book.best_bid().unwrap().price, dec!(0.5));
|
||||
assert_eq!(book.best_bid().unwrap().size, dec!(100));
|
||||
assert_eq!(book.sequence, 1); // Sequence should update
|
||||
assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); // Should be our bid
|
||||
assert_eq!(book.best_bid().unwrap().size, dec!(100)); // Should be our size
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spread_calculation() {
|
||||
// Test that we can calculate the spread between bid and ask
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
// Add bid
|
||||
// Add a bid at $0.50
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
@@ -462,7 +559,7 @@ mod tests {
|
||||
sequence: 1,
|
||||
}).unwrap();
|
||||
|
||||
// Add ask
|
||||
// Add an ask at $0.52
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
@@ -473,14 +570,16 @@ mod tests {
|
||||
}).unwrap();
|
||||
|
||||
let spread = book.spread().unwrap();
|
||||
assert_eq!(spread, dec!(0.02));
|
||||
assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_impact() {
|
||||
// Test market impact calculation for a large order
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
// Add multiple ask levels
|
||||
// Add multiple ask levels (people selling at different prices)
|
||||
// $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens
|
||||
for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
@@ -492,8 +591,9 @@ mod tests {
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
// Try to buy 150 tokens (will need to hit multiple price levels)
|
||||
let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap();
|
||||
assert!(impact.average_price > dec!(0.50));
|
||||
assert!(impact.average_price < dec!(0.51));
|
||||
assert!(impact.average_price > dec!(0.50)); // Should be worse than best price
|
||||
assert!(impact.average_price < dec!(0.51)); // But not as bad as second level
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Polyfill-rs: High-performance Rust client for Polymarket
|
||||
//!
|
||||
//! A production-ready Rust client for Polymarket optimized for high-frequency trading.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - **High-performance order book management** with optimized data structures
|
||||
|
||||
Reference in New Issue
Block a user