Skip to main content

polyfill_rs/
fill.rs

1//! Trade execution simulation and fill handling for the Polymarket client.
2//!
3//! This module is intended for local simulation, slippage checks, and fill-event
4//! bookkeeping. It is not a hot-path matching or execution engine: market-order
5//! simulation materializes book levels as `Decimal` values, creates UUID-backed
6//! fill IDs, clones order identifiers, and stores owned fill history.
7
8use crate::errors::{PolyfillError, Result};
9use crate::types::*;
10use crate::utils::math;
11use alloy_primitives::Address;
12use chrono::{DateTime, Utc};
13use rust_decimal::Decimal;
14use std::collections::HashMap;
15use tracing::{debug, info, warn};
16
17/// Fill execution result
18#[derive(Debug, Clone)]
19pub struct FillResult {
20    pub order_id: String,
21    pub fills: Vec<FillEvent>,
22    pub total_size: Decimal,
23    pub average_price: Decimal,
24    pub total_cost: Decimal,
25    pub fees: Decimal,
26    pub status: FillStatus,
27    pub timestamp: DateTime<Utc>,
28}
29
30/// Fill execution status
31#[derive(Debug, Clone, PartialEq)]
32pub enum FillStatus {
33    /// Order was fully filled
34    Filled,
35    /// Order was partially filled
36    Partial,
37    /// Order was not filled (insufficient liquidity)
38    Unfilled,
39    /// Order was rejected
40    Rejected,
41}
42
43/// Fill simulation and bookkeeping engine.
44///
45/// `FillEngine` favors ergonomic `Decimal` outputs and owned `FillEvent` records
46/// over allocation-sensitive execution. It should not be used as evidence that the
47/// order-routing or market-data hot paths are allocation-free.
48#[derive(Debug)]
49pub struct FillEngine {
50    /// Minimum fill size for market orders
51    min_fill_size: Decimal,
52    /// Maximum slippage tolerance (as percentage)
53    max_slippage_pct: Decimal,
54    /// Fee rate in basis points
55    fee_rate_bps: u32,
56    /// Track fills by order ID
57    fills: HashMap<String, Vec<FillEvent>>,
58}
59
60impl FillEngine {
61    /// Create a new fill simulation engine.
62    pub fn new(min_fill_size: Decimal, max_slippage_pct: Decimal, fee_rate_bps: u32) -> Self {
63        Self {
64            min_fill_size,
65            max_slippage_pct,
66            fee_rate_bps,
67            fills: HashMap::new(),
68        }
69    }
70
71    /// Simulate executing a market order against an order book.
72    ///
73    /// This method allocates and converts internal book levels back to `Decimal`
74    /// via `OrderBook::asks`/`OrderBook::bids`, then builds owned `FillEvent`
75    /// records. It is suitable for simulation and reporting, not latency-sensitive
76    /// execution benchmarking.
77    pub fn execute_market_order(
78        &mut self,
79        order: &MarketOrderRequest,
80        book: &crate::book::OrderBook,
81    ) -> Result<FillResult> {
82        let start_time = Utc::now();
83
84        // Validate order
85        self.validate_market_order(order)?;
86
87        // Get available liquidity
88        let levels = match order.side {
89            Side::BUY => book.asks(None),
90            Side::SELL => book.bids(None),
91        };
92
93        if levels.is_empty() {
94            return Ok(FillResult {
95                order_id: order
96                    .client_id
97                    .clone()
98                    .unwrap_or_else(|| "market_order".to_string()),
99                fills: Vec::new(),
100                total_size: Decimal::ZERO,
101                average_price: Decimal::ZERO,
102                total_cost: Decimal::ZERO,
103                fees: Decimal::ZERO,
104                status: FillStatus::Unfilled,
105                timestamp: start_time,
106            });
107        }
108
109        // Execute fills
110        let mut fills = Vec::new();
111        let mut remaining_size = order.amount;
112        let mut total_cost = Decimal::ZERO;
113        let mut total_size = Decimal::ZERO;
114
115        for level in levels {
116            if remaining_size.is_zero() {
117                break;
118            }
119
120            let fill_size = std::cmp::min(remaining_size, level.size);
121            let fill_cost = fill_size * level.price;
122
123            // Calculate fee
124            let fee = self.calculate_fee(fill_cost);
125
126            let fill = FillEvent {
127                id: uuid::Uuid::new_v4().to_string(),
128                order_id: order
129                    .client_id
130                    .clone()
131                    .unwrap_or_else(|| "market_order".to_string()),
132                token_id: order.token_id.clone(),
133                side: order.side,
134                price: level.price,
135                size: fill_size,
136                timestamp: Utc::now(),
137                maker_address: Address::ZERO, // TODO: Get from level
138                taker_address: Address::ZERO, // TODO: Get from order
139                fee,
140            };
141
142            fills.push(fill);
143            total_cost += fill_cost;
144            total_size += fill_size;
145            remaining_size -= fill_size;
146        }
147
148        // Check slippage
149        if let Some(slippage) = self.calculate_slippage(order, &fills) {
150            if slippage > self.max_slippage_pct {
151                warn!(
152                    "Slippage {}% exceeds maximum {}%",
153                    slippage, self.max_slippage_pct
154                );
155                return Ok(FillResult {
156                    order_id: order
157                        .client_id
158                        .clone()
159                        .unwrap_or_else(|| "market_order".to_string()),
160                    fills: Vec::new(),
161                    total_size: Decimal::ZERO,
162                    average_price: Decimal::ZERO,
163                    total_cost: Decimal::ZERO,
164                    fees: Decimal::ZERO,
165                    status: FillStatus::Rejected,
166                    timestamp: start_time,
167                });
168            }
169        }
170
171        // Determine status
172        let status = if remaining_size.is_zero() {
173            FillStatus::Filled
174        } else if total_size >= self.min_fill_size {
175            FillStatus::Partial
176        } else {
177            FillStatus::Unfilled
178        };
179
180        let average_price = if total_size.is_zero() {
181            Decimal::ZERO
182        } else {
183            total_cost / total_size
184        };
185
186        let total_fees: Decimal = fills.iter().map(|f| f.fee).sum();
187
188        let result = FillResult {
189            order_id: order
190                .client_id
191                .clone()
192                .unwrap_or_else(|| "market_order".to_string()),
193            fills,
194            total_size,
195            average_price,
196            total_cost,
197            fees: total_fees,
198            status,
199            timestamp: start_time,
200        };
201
202        // Store fills for tracking
203        if !result.fills.is_empty() {
204            self.fills
205                .insert(result.order_id.clone(), result.fills.clone());
206        }
207
208        info!(
209            "Market order executed: {} {} @ {} (avg: {})",
210            result.total_size,
211            order.side.as_str(),
212            order.amount,
213            result.average_price
214        );
215
216        Ok(result)
217    }
218
219    /// Simulate executing a limit order.
220    pub fn execute_limit_order(
221        &mut self,
222        order: &OrderRequest,
223        book: &crate::book::OrderBook,
224    ) -> Result<FillResult> {
225        let start_time = Utc::now();
226
227        // Validate order
228        self.validate_limit_order(order)?;
229
230        // Check if order can be filled immediately
231        let can_fill = match order.side {
232            Side::BUY => {
233                if let Some(best_ask) = book.best_ask() {
234                    order.price >= best_ask.price
235                } else {
236                    false
237                }
238            },
239            Side::SELL => {
240                if let Some(best_bid) = book.best_bid() {
241                    order.price <= best_bid.price
242                } else {
243                    false
244                }
245            },
246        };
247
248        if !can_fill {
249            return Ok(FillResult {
250                order_id: order
251                    .client_id
252                    .clone()
253                    .unwrap_or_else(|| "limit_order".to_string()),
254                fills: Vec::new(),
255                total_size: Decimal::ZERO,
256                average_price: Decimal::ZERO,
257                total_cost: Decimal::ZERO,
258                fees: Decimal::ZERO,
259                status: FillStatus::Unfilled,
260                timestamp: start_time,
261            });
262        }
263
264        // Simulate immediate fill
265        let fill = FillEvent {
266            id: uuid::Uuid::new_v4().to_string(),
267            order_id: order
268                .client_id
269                .clone()
270                .unwrap_or_else(|| "limit_order".to_string()),
271            token_id: order.token_id.clone(),
272            side: order.side,
273            price: order.price,
274            size: order.size,
275            timestamp: Utc::now(),
276            maker_address: Address::ZERO,
277            taker_address: Address::ZERO,
278            fee: self.calculate_fee(order.price * order.size),
279        };
280
281        let result = FillResult {
282            order_id: order
283                .client_id
284                .clone()
285                .unwrap_or_else(|| "limit_order".to_string()),
286            fills: vec![fill],
287            total_size: order.size,
288            average_price: order.price,
289            total_cost: order.price * order.size,
290            fees: self.calculate_fee(order.price * order.size),
291            status: FillStatus::Filled,
292            timestamp: start_time,
293        };
294
295        // Store fills for tracking
296        self.fills
297            .insert(result.order_id.clone(), result.fills.clone());
298
299        info!(
300            "Limit order executed: {} {} @ {}",
301            result.total_size,
302            order.side.as_str(),
303            result.average_price
304        );
305
306        Ok(result)
307    }
308
309    /// Calculate slippage for a market order
310    fn calculate_slippage(
311        &self,
312        order: &MarketOrderRequest,
313        fills: &[FillEvent],
314    ) -> Option<Decimal> {
315        if fills.is_empty() {
316            return None;
317        }
318
319        let total_cost: Decimal = fills.iter().map(|f| f.price * f.size).sum();
320        let total_size: Decimal = fills.iter().map(|f| f.size).sum();
321        let average_price = total_cost / total_size;
322
323        // Get reference price (best bid/ask)
324        let reference_price = match order.side {
325            Side::BUY => fills.first()?.price,  // Best ask
326            Side::SELL => fills.first()?.price, // Best bid
327        };
328
329        Some(math::calculate_slippage(
330            reference_price,
331            average_price,
332            order.side,
333        ))
334    }
335
336    /// Calculate fee for a trade
337    fn calculate_fee(&self, notional: Decimal) -> Decimal {
338        notional * Decimal::from(self.fee_rate_bps) / Decimal::from(10_000)
339    }
340
341    /// Validate market order parameters
342    fn validate_market_order(&self, order: &MarketOrderRequest) -> Result<()> {
343        if order.amount.is_zero() {
344            return Err(PolyfillError::order(
345                "Market order amount cannot be zero",
346                crate::errors::OrderErrorKind::InvalidSize,
347            ));
348        }
349
350        if order.amount < self.min_fill_size {
351            return Err(PolyfillError::order(
352                format!(
353                    "Order size {} below minimum {}",
354                    order.amount, self.min_fill_size
355                ),
356                crate::errors::OrderErrorKind::SizeConstraint,
357            ));
358        }
359
360        Ok(())
361    }
362
363    /// Validate limit order parameters
364    fn validate_limit_order(&self, order: &OrderRequest) -> Result<()> {
365        if order.size.is_zero() {
366            return Err(PolyfillError::order(
367                "Limit order size cannot be zero",
368                crate::errors::OrderErrorKind::InvalidSize,
369            ));
370        }
371
372        if order.price.is_zero() {
373            return Err(PolyfillError::order(
374                "Limit order price cannot be zero",
375                crate::errors::OrderErrorKind::InvalidPrice,
376            ));
377        }
378
379        if order.size < self.min_fill_size {
380            return Err(PolyfillError::order(
381                format!(
382                    "Order size {} below minimum {}",
383                    order.size, self.min_fill_size
384                ),
385                crate::errors::OrderErrorKind::SizeConstraint,
386            ));
387        }
388
389        Ok(())
390    }
391
392    /// Get fills for an order
393    pub fn get_fills(&self, order_id: &str) -> Option<&[FillEvent]> {
394        self.fills.get(order_id).map(|f| f.as_slice())
395    }
396
397    /// Get all fills
398    pub fn get_all_fills(&self) -> Vec<&FillEvent> {
399        self.fills.values().flatten().collect()
400    }
401
402    /// Clear fills for an order
403    pub fn clear_fills(&mut self, order_id: &str) {
404        self.fills.remove(order_id);
405    }
406
407    /// Get fill statistics
408    pub fn get_stats(&self) -> FillStats {
409        let total_fills = self.fills.values().flatten().count();
410        let total_volume: Decimal = self.fills.values().flatten().map(|f| f.size).sum();
411        let total_fees: Decimal = self.fills.values().flatten().map(|f| f.fee).sum();
412
413        FillStats {
414            total_orders: self.fills.len(),
415            total_fills,
416            total_volume,
417            total_fees,
418        }
419    }
420}
421
422/// Fill statistics
423#[derive(Debug, Clone)]
424pub struct FillStats {
425    pub total_orders: usize,
426    pub total_fills: usize,
427    pub total_volume: Decimal,
428    pub total_fees: Decimal,
429}
430
431/// Fill event processor for real-time updates
432#[derive(Debug)]
433pub struct FillProcessor {
434    /// Pending fills by order ID
435    pending_fills: HashMap<String, Vec<FillEvent>>,
436    /// Processed fills
437    processed_fills: Vec<FillEvent>,
438    /// Maximum pending fills to keep in memory
439    max_pending: usize,
440}
441
442impl FillProcessor {
443    /// Create a new fill processor
444    pub fn new(max_pending: usize) -> Self {
445        Self {
446            pending_fills: HashMap::new(),
447            processed_fills: Vec::new(),
448            max_pending,
449        }
450    }
451
452    /// Process a fill event
453    pub fn process_fill(&mut self, fill: FillEvent) -> Result<()> {
454        // Validate fill
455        self.validate_fill(&fill)?;
456
457        // Add to pending fills
458        self.pending_fills
459            .entry(fill.order_id.clone())
460            .or_default()
461            .push(fill.clone());
462
463        // Move to processed if complete
464        if self.is_order_complete(&fill.order_id) {
465            if let Some(fills) = self.pending_fills.remove(&fill.order_id) {
466                self.processed_fills.extend(fills);
467            }
468        }
469
470        // Cleanup if too many pending
471        if self.pending_fills.len() > self.max_pending {
472            self.cleanup_old_pending();
473        }
474
475        debug!(
476            "Processed fill: {} {} @ {}",
477            fill.size,
478            fill.side.as_str(),
479            fill.price
480        );
481
482        Ok(())
483    }
484
485    /// Validate a fill event
486    fn validate_fill(&self, fill: &FillEvent) -> Result<()> {
487        if fill.size.is_zero() {
488            return Err(PolyfillError::order(
489                "Fill size cannot be zero",
490                crate::errors::OrderErrorKind::InvalidSize,
491            ));
492        }
493
494        if fill.price.is_zero() {
495            return Err(PolyfillError::order(
496                "Fill price cannot be zero",
497                crate::errors::OrderErrorKind::InvalidPrice,
498            ));
499        }
500
501        Ok(())
502    }
503
504    /// Check if an order is complete
505    fn is_order_complete(&self, _order_id: &str) -> bool {
506        // Simplified implementation - in practice you'd check against order book
507        false
508    }
509
510    /// Cleanup old pending fills
511    fn cleanup_old_pending(&mut self) {
512        // Remove oldest pending fills
513        let to_remove = self.pending_fills.len() - self.max_pending;
514        let mut keys: Vec<_> = self.pending_fills.keys().cloned().collect();
515        keys.sort(); // Simple ordering - in practice you'd use timestamps
516
517        for key in keys.iter().take(to_remove) {
518            self.pending_fills.remove(key);
519        }
520    }
521
522    /// Get pending fills for an order
523    pub fn get_pending_fills(&self, order_id: &str) -> Option<&[FillEvent]> {
524        self.pending_fills.get(order_id).map(|f| f.as_slice())
525    }
526
527    /// Get processed fills
528    pub fn get_processed_fills(&self) -> &[FillEvent] {
529        &self.processed_fills
530    }
531
532    /// Get fill statistics
533    pub fn get_stats(&self) -> FillProcessorStats {
534        let total_pending: Decimal = self.pending_fills.values().flatten().map(|f| f.size).sum();
535        let total_processed: Decimal = self.processed_fills.iter().map(|f| f.size).sum();
536
537        FillProcessorStats {
538            pending_orders: self.pending_fills.len(),
539            pending_fills: self.pending_fills.values().flatten().count(),
540            pending_volume: total_pending,
541            processed_fills: self.processed_fills.len(),
542            processed_volume: total_processed,
543        }
544    }
545}
546
547/// Fill processor statistics
548#[derive(Debug, Clone)]
549pub struct FillProcessorStats {
550    pub pending_orders: usize,
551    pub pending_fills: usize,
552    pub pending_volume: Decimal,
553    pub processed_fills: usize,
554    pub processed_volume: Decimal,
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use rust_decimal_macros::dec;
561
562    #[test]
563    fn test_fill_engine_creation() {
564        let engine = FillEngine::new(dec!(1), dec!(5), 10);
565        assert_eq!(engine.min_fill_size, dec!(1));
566        assert_eq!(engine.max_slippage_pct, dec!(5));
567        assert_eq!(engine.fee_rate_bps, 10);
568    }
569
570    #[test]
571    fn test_market_order_validation() {
572        let engine = FillEngine::new(dec!(1), dec!(5), 10);
573
574        let valid_order = MarketOrderRequest {
575            token_id: "test".to_string(),
576            side: Side::BUY,
577            amount: dec!(100),
578            slippage_tolerance: None,
579            client_id: None,
580        };
581        assert!(engine.validate_market_order(&valid_order).is_ok());
582
583        let invalid_order = MarketOrderRequest {
584            token_id: "test".to_string(),
585            side: Side::BUY,
586            amount: dec!(0),
587            slippage_tolerance: None,
588            client_id: None,
589        };
590        assert!(engine.validate_market_order(&invalid_order).is_err());
591    }
592
593    #[test]
594    fn test_fee_calculation() {
595        let engine = FillEngine::new(dec!(1), dec!(5), 10);
596        let fee = engine.calculate_fee(dec!(1000));
597        assert_eq!(fee, dec!(1)); // 10 bps = 0.1% = 1 on 1000
598    }
599
600    #[test]
601    fn test_fill_processor() {
602        let mut processor = FillProcessor::new(100);
603
604        let fill = FillEvent {
605            id: "fill1".to_string(),
606            order_id: "order1".to_string(),
607            token_id: "test".to_string(),
608            side: Side::BUY,
609            price: dec!(0.5),
610            size: dec!(100),
611            timestamp: Utc::now(),
612            maker_address: Address::ZERO,
613            taker_address: Address::ZERO,
614            fee: dec!(0.1),
615        };
616
617        assert!(processor.process_fill(fill).is_ok());
618        assert_eq!(processor.pending_fills.len(), 1);
619    }
620
621    #[test]
622    fn test_fill_engine_advanced_creation() {
623        // Test that we can create a fill engine with parameters
624        let _engine = FillEngine::new(dec!(1.0), dec!(0.05), 50); // min_fill_size, max_slippage, fee_rate_bps
625
626        // Test basic properties exist (we can't access private fields directly)
627        // But we can test that the engine was created successfully
628        // Engine creation successful
629    }
630
631    #[test]
632    fn test_fill_processor_basic_operations() {
633        let mut processor = FillProcessor::new(100); // max_pending
634
635        // Test that we can create a fill event and process it
636        let fill_event = FillEvent {
637            id: "fill_1".to_string(),
638            order_id: "order_1".to_string(),
639            side: Side::BUY,
640            size: dec!(25),
641            price: dec!(0.75),
642            timestamp: chrono::Utc::now(),
643            token_id: "token_1".to_string(),
644            maker_address: alloy_primitives::Address::ZERO,
645            taker_address: alloy_primitives::Address::ZERO,
646            fee: dec!(0.01),
647        };
648
649        let result = processor.process_fill(fill_event);
650        assert!(result.is_ok());
651
652        // Check that the fill was added to pending
653        assert_eq!(processor.pending_fills.len(), 1);
654    }
655}