//! Snipe example for polyfill-rs //! //! This example demonstrates high-frequency trading techniques including: //! - Real-time order book monitoring //! - Stale quote detection //! - Rapid order execution //! - Market impact analysis use polyfill_rs::{ book::OrderBookManager, errors::Result, fill::{FillEngine, FillStatus}, types::*, utils::time, }; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::time::Duration; use tokio::time::sleep; use tracing::{error, info, warn}; /// Snipe trading strategy #[derive(Debug)] pub struct SnipeStrategy { /// Target token ID token_id: String, /// Maximum spread to consider max_spread_pct: Decimal, /// Minimum order size min_order_size: Decimal, /// Maximum order size max_order_size: Decimal, /// Stale quote threshold (seconds) stale_threshold: u64, /// Last known best prices last_best_bid: Option, last_best_ask: Option, /// Last update timestamp last_update: u64, /// Order book manager book_manager: OrderBookManager, /// Fill engine fill_engine: FillEngine, /// Statistics stats: SnipeStats, } /// Snipe trading statistics #[derive(Debug, Clone)] pub struct SnipeStats { pub opportunities_detected: u64, pub orders_placed: u64, pub orders_filled: u64, pub total_volume: Decimal, pub total_pnl: Decimal, pub avg_fill_time_ms: f64, } impl Default for SnipeStats { fn default() -> Self { Self { opportunities_detected: 0, orders_placed: 0, orders_filled: 0, total_volume: dec!(0), total_pnl: dec!(0), avg_fill_time_ms: 0.0, } } } impl SnipeStrategy { /// Create a new snipe strategy pub fn new( token_id: String, max_spread_pct: Decimal, min_order_size: Decimal, max_order_size: Decimal, stale_threshold: u64, ) -> Self { Self { token_id, max_spread_pct, min_order_size, max_order_size, stale_threshold, last_best_bid: None, last_best_ask: None, last_update: 0, book_manager: OrderBookManager::new(100), fill_engine: FillEngine::new( min_order_size, dec!(2.0), // 2% max slippage 5, // 5 bps fee rate ), stats: SnipeStats::default(), } } /// Process a market data update pub fn process_update(&mut self, message: StreamMessage) -> Result<()> { match message { StreamMessage::Book(book) if book.asset_id == self.token_id => { self.process_book_update(book)?; }, StreamMessage::Trade(trade) if trade.asset_id == self.token_id => { self.process_trade(trade)?; }, _ => {}, } // Opportunistically check for staleness on any incoming update. self.check_stale_quotes()?; Ok(()) } /// Process order book update fn process_book_update(&mut self, book: BookUpdate) -> Result<()> { // Ensure book exists self.book_manager.get_or_create_book(&self.token_id)?; // Clear the existing book and rebuild from the snapshot. if let Ok(current) = self.book_manager.get_book(&self.token_id) { for level in ¤t.bids { let _ = self.book_manager.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), side: Side::BUY, price: level.price, size: Decimal::ZERO, sequence: book.timestamp, }); } for level in ¤t.asks { let _ = self.book_manager.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), side: Side::SELL, price: level.price, size: Decimal::ZERO, sequence: book.timestamp, }); } } let ts = chrono::DateTime::from_timestamp( (book.timestamp / 1000) as i64, ((book.timestamp % 1000) * 1_000_000) as u32, ) .unwrap_or_else(chrono::Utc::now); for level in &book.bids { let _ = self.book_manager.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: ts, side: Side::BUY, price: level.price, size: level.size, sequence: book.timestamp, }); } for level in &book.asks { let _ = self.book_manager.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: ts, side: Side::SELL, price: level.price, size: level.size, sequence: book.timestamp, }); } // Update best prices directly from the snapshot self.last_best_bid = book.bids.first().map(|l| l.price); self.last_best_ask = book.asks.first().map(|l| l.price); self.last_update = time::now_secs(); // Check for trading opportunities self.check_opportunities()?; Ok(()) } /// Process trade update fn process_trade(&mut self, trade: TradeMessage) -> Result<()> { info!( "Trade: {} {} @ {} (size: {})", trade.side.as_str(), trade.asset_id, trade.price, trade.size ); // Update statistics self.stats.total_volume += trade.size; // Calculate P&L if this was our trade // (In a real implementation, you'd track your own orders) Ok(()) } /// Check for trading opportunities fn check_opportunities(&mut self) -> Result<()> { let (bid, ask) = match (self.last_best_bid, self.last_best_ask) { (Some(bid), Some(ask)) => (bid, ask), _ => return Ok(()), // No liquidity }; // Calculate spread let spread_pct = match (bid, ask) { (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 ); // Execute snipe order self.execute_snipe_order(bid, ask)?; } Ok(()) } /// Execute a snipe order 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::() % 100) / Decimal::from(100); 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 { Side::SELL // Crossed market, sell } else { Side::BUY // Normal market, buy }; // Create market order request let request = MarketOrderRequest { token_id: self.token_id.clone(), side, amount: size, slippage_tolerance: Some(dec!(1.0)), // 1% slippage tolerance client_id: Some(format!("snipe_{}", time::now_millis())), }; // 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 { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), side: Side::BUY, price: level.price, size: level.size, sequence: 1, })?; } for level in &book.asks { book_impl.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), side: Side::SELL, price: level.price, size: level.size, sequence: 2, })?; } // Execute order let start_time = std::time::Instant::now(); let result = self .fill_engine .execute_market_order(&request, &book_impl)?; let fill_time = start_time.elapsed().as_millis() as f64; // Update statistics self.stats.orders_placed += 1; if result.status == FillStatus::Filled { self.stats.orders_filled += 1; } // Update average 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!( "Snipe order executed: {} {} @ {} (fill time: {}ms)", result.total_size, side.as_str(), result.average_price, fill_time ); Ok(()) } /// Check for stale quotes fn check_stale_quotes(&mut self) -> Result<()> { let now = time::now_secs(); let age = now.saturating_sub(self.last_update); if age > self.stale_threshold { warn!( "Stale quotes detected: {}s old (threshold: {}s)", age, self.stale_threshold ); // In a real implementation, you might: // - Cancel pending orders // - Switch to a different data source // - Reduce position sizes // - Stop trading temporarily } Ok(()) } /// Get current statistics pub fn get_stats(&self) -> &SnipeStats { &self.stats } } /// Mock market data generator for testing struct MockMarketData { token_id: String, base_price: Decimal, volatility: Decimal, sequence: u64, } impl MockMarketData { fn new(token_id: String, base_price: Decimal) -> Self { Self { token_id, base_price, volatility: dec!(0.01), // 1% volatility sequence: 0, } } fn generate_update(&mut self) -> StreamMessage { self.sequence += 1; // Generate random price movement let random_factor = Decimal::from(rand::random::() % 100 - 50) / Decimal::from(100); let _volatility_f64 = self.volatility.to_f64().unwrap_or(0.01); let price_change = random_factor * Decimal::from(2) * self.volatility; let new_price = self.base_price * (Decimal::from(1) + price_change); // Generate a simple orderbook snapshot update let size = Decimal::from(rand::random::() % 1000 + 100); let bid = new_price - dec!(0.01); let ask = new_price + dec!(0.01); StreamMessage::Book(BookUpdate { asset_id: self.token_id.clone(), market: "0xmock".to_string(), timestamp: time::now_millis(), bids: vec![OrderSummary { price: bid, size }], asks: vec![OrderSummary { price: ask, size }], hash: None, }) } } #[tokio::main] async fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt::init(); info!("Starting snipe trading example..."); // Create snipe strategy let mut strategy = SnipeStrategy::new( "12345".to_string(), // Example token ID dec!(2.0), // 2% max spread dec!(10), // Min order size dec!(100), // Max order size 5, // 5 second stale threshold ); // Create mock market data generator let mut market_data = MockMarketData::new( "12345".to_string(), dec!(0.5), // Base price $0.50 ); // Simulate market data stream let mut message_count = 0; let max_messages = 100; 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); } // Print statistics every 10 messages if message_count % 10 == 0 { let stats = strategy.get_stats(); info!( "Stats: {} opportunities, {} orders placed, {} filled, avg fill time: {:.2}ms", stats.opportunities_detected, stats.orders_placed, stats.orders_filled, stats.avg_fill_time_ms ); } message_count += 1; sleep(Duration::from_millis(100)).await; // 100ms between updates } // Print final statistics let final_stats = strategy.get_stats(); info!("Final statistics:"); 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); info!(" Average fill time: {:.2}ms", final_stats.avg_fill_time_ms); info!("Snipe trading example completed!"); Ok(()) }