rusty_model/data/
market_trade.rs

1//! Market trade data structures
2//!
3//! This module provides structures for representing executed trades in the market,
4//! including individual trades and efficient batch storage for HFT systems.
5
6use crate::enums::OrderSide;
7use crate::instruments::InstrumentId;
8use crate::simd::SimdOps;
9use quanta::{Clock, Instant};
10use rust_decimal::Decimal;
11use rust_decimal::prelude::FromPrimitive;
12use smallvec::SmallVec;
13
14/// Represents a single executed trade in the market
15///
16/// Cache-aligned structure for optimal memory access patterns in HFT systems.
17/// Contains all essential information about a market trade/transaction.
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[repr(align(64))] // Cache-line aligned for HFT performance
20pub struct MarketTrade {
21    /// High-resolution timestamp using `quanta`.
22    pub timestamp: Instant,
23    /// Exchange-provided timestamp (nanoseconds).
24    pub exchange_time_ns: u64,
25    /// Trade price.
26    pub price: Decimal,
27    /// Trade quantity.
28    pub quantity: Decimal,
29    /// Buy or Sell side.
30    pub direction: OrderSide,
31    /// Reference to the traded instrument.
32    pub instrument_id: InstrumentId,
33}
34
35/// Efficient storage for a batch of trades using cache-line alignment
36/// and zero-allocation strategies for HFT applications
37#[derive(Debug)]
38#[repr(align(64))]
39pub struct TradeBatch {
40    /// Store trades in `SmallVec` to avoid heap allocations for typical trade batches
41    trades: SmallVec<[MarketTrade; 128]>,
42
43    /// Shared clock for high-precision timestamp generation
44    clock: Clock,
45
46    /// Instrument identifier this batch is for
47    instrument_id: InstrumentId,
48
49    /// Last update timestamp (nanoseconds)
50    last_update: u64,
51}
52
53impl MarketTrade {
54    /// Creates a new trade with current timestamp
55    #[inline]
56    #[must_use]
57    pub fn new(
58        price: Decimal,
59        quantity: Decimal,
60        direction: OrderSide,
61        instrument_id: InstrumentId,
62        clock: &Clock,
63        exchange_time_ns: u64,
64    ) -> Self {
65        Self {
66            timestamp: clock.now(),
67            exchange_time_ns,
68            price,
69            quantity,
70            direction,
71            instrument_id,
72        }
73    }
74
75    /// Get the latency between exchange time and local time (in nanoseconds)
76    #[inline]
77    #[must_use]
78    pub fn latency(&self) -> Option<u64> {
79        if self.exchange_time_ns == 0 {
80            return None;
81        }
82
83        let local_time = u64::try_from(self.timestamp.duration_since(Instant::recent()).as_nanos())
84            .unwrap_or(u64::MAX);
85        Some(local_time.saturating_sub(self.exchange_time_ns))
86    }
87
88    /// Get the notional value (price * quantity) of this trade
89    #[inline]
90    #[must_use]
91    pub fn notional_value(&self) -> Decimal {
92        self.price * self.quantity
93    }
94}
95
96impl TradeBatch {
97    /// Creates a new, empty `TradeBatch` with pre-allocated capacity
98    #[inline]
99    #[must_use]
100    pub fn new(instrument_id: InstrumentId) -> Self {
101        Self {
102            trades: SmallVec::with_capacity(16),
103            clock: Clock::new(),
104            instrument_id,
105            last_update: 0,
106        }
107    }
108
109    /// Creates a new `TradeBatch` with the specified clock
110    #[inline]
111    #[must_use]
112    pub fn with_clock(instrument_id: InstrumentId, clock: Clock) -> Self {
113        Self {
114            trades: SmallVec::with_capacity(16),
115            clock,
116            instrument_id,
117            last_update: 0,
118        }
119    }
120
121    /// Adds a new trade to the batch with minimal allocation
122    ///
123    /// This method creates a new trade and inserts it into the batch in chronological order
124    /// using binary search to find the correct insertion position. This ensures that trades
125    /// are always sorted by timestamp, which improves the performance of time-based queries.
126    ///
127    /// # Performance considerations
128    /// - Uses binary search for O(log n) insertion position finding
129    /// - Maintains chronological order without sorting the entire vector
130    /// - Updates the `last_update` timestamp atomically
131    #[inline]
132    pub fn add_trade(
133        &mut self,
134        price: Decimal,
135        quantity: Decimal,
136        direction: OrderSide,
137        exchange_time_ns: u64,
138    ) {
139        let trade = MarketTrade::new(
140            price,
141            quantity,
142            direction,
143            self.instrument_id.clone(),
144            &self.clock,
145            exchange_time_ns,
146        );
147
148        // Find the correct insertion position using binary search
149        // This ensures that trades are always sorted by timestamp
150        let insert_pos = match self
151            .trades
152            .binary_search_by(|existing| existing.timestamp.cmp(&trade.timestamp))
153        {
154            // Exact match (unlikely with high-precision timestamps) or insertion position
155            Ok(pos) | Err(pos) => pos,
156        };
157
158        self.trades.insert(insert_pos, trade);
159        self.last_update = self.clock.raw();
160    }
161
162    /// Returns a reference to all trades in this batch
163    #[inline]
164    #[must_use]
165    pub fn trades(&self) -> &[MarketTrade] {
166        &self.trades
167    }
168
169    /// Returns the number of trades in this batch
170    #[inline]
171    #[must_use]
172    pub fn len(&self) -> usize {
173        self.trades.len()
174    }
175
176    /// Returns true if this batch is empty
177    #[inline]
178    #[must_use]
179    pub fn is_empty(&self) -> bool {
180        self.trades.is_empty()
181    }
182
183    /// Gets the instrument ID for this trade batch
184    #[inline]
185    #[must_use]
186    pub const fn instrument_id(&self) -> &InstrumentId {
187        &self.instrument_id
188    }
189
190    /// Gets the last update timestamp
191    #[inline]
192    #[must_use]
193    pub const fn last_update(&self) -> u64 {
194        self.last_update
195    }
196
197    /// Filters trades by a specified direction without allocation
198    /// Returns a count rather than a collection to avoid allocations
199    #[inline]
200    #[must_use]
201    pub fn count_by_direction(&self, direction: OrderSide) -> usize {
202        self.trades
203            .iter()
204            .filter(|t| t.direction == direction)
205            .count()
206    }
207
208    /// Returns a filtered iterator for trades with the specified direction
209    #[inline]
210    pub fn iter_by_direction(
211        &self,
212        direction: OrderSide,
213    ) -> impl Iterator<Item = &MarketTrade> + '_ {
214        self.trades.iter().filter(move |t| t.direction == direction)
215    }
216
217    /// Filters trades that occurred after a specific timestamp
218    /// Returns an iterator to avoid allocations
219    #[inline]
220    pub fn iter_after(&self, timestamp: Instant) -> impl Iterator<Item = &MarketTrade> + '_ {
221        self.trades.iter().filter(move |t| t.timestamp > timestamp)
222    }
223
224    /// Filters trades that occurred before a specific timestamp
225    /// Returns an iterator to avoid allocations
226    #[inline]
227    pub fn iter_before(&self, timestamp: Instant) -> impl Iterator<Item = &MarketTrade> + '_ {
228        self.trades.iter().filter(move |t| t.timestamp < timestamp)
229    }
230
231    /// Computes the total volume in the batch
232    ///
233    /// This method is optimized for high-frequency trading using SIMD instructions
234    /// when available. It extracts quantity values from trades and uses SIMD-accelerated
235    /// summation for maximum performance.
236    ///
237    /// # Performance considerations
238    /// - Uses SIMD instructions for parallel processing when available
239    /// - Falls back to scalar operations for small trade counts
240    /// - Avoids heap allocations for small to medium-sized batches
241    /// - Optimized for both small and large trade batches
242    #[inline]
243    #[must_use]
244    pub fn total_volume(&self) -> Decimal {
245        if self.trades.is_empty() {
246            return Decimal::ZERO;
247        }
248
249        if self.trades.len() == 1 {
250            return self.trades[0].quantity;
251        }
252
253        // For very small batches, use scalar sum directly
254        if self.trades.len() <= 3 {
255            return self
256                .trades
257                .iter()
258                .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
259        }
260
261        // For small to medium batches, use stack allocation to avoid heap allocation
262        if self.trades.len() <= 64 {
263            let mut quantities = [Decimal::ZERO; 64];
264            for (i, trade) in self.trades.iter().enumerate() {
265                if i >= 64 {
266                    break;
267                }
268                quantities[i] = trade.quantity;
269            }
270
271            // Use SIMD-accelerated sum on the stack-allocated array
272            return SimdOps::sum_decimal(&quantities[..self.trades.len()]);
273        }
274
275        // For larger batches, extract quantities and use SIMD-accelerated sum
276        let quantities: Vec<Decimal> = self.trades.iter().map(|t| t.quantity).collect();
277        SimdOps::sum_decimal(&quantities)
278    }
279
280    /// Computes the buy volume in the batch
281    ///
282    /// This method is optimized for high-frequency trading using SIMD instructions
283    /// when available. It filters trades by direction and uses SIMD-accelerated
284    /// summation for maximum performance.
285    ///
286    /// # Performance considerations
287    /// - Uses SIMD instructions for parallel processing when available
288    /// - Falls back to scalar operations for small trade counts
289    /// - Avoids heap allocations for small to medium-sized batches
290    /// - Optimized for both small and large trade batches
291    #[inline]
292    #[must_use]
293    pub fn buy_volume(&self) -> Decimal {
294        if self.trades.is_empty() {
295            return Decimal::ZERO;
296        }
297
298        // Count buy trades to optimize allocation strategy
299        let buy_count = self.count_by_direction(OrderSide::Buy);
300
301        if buy_count == 0 {
302            return Decimal::ZERO;
303        }
304
305        if buy_count == 1 {
306            // Find the single buy trade and return its quantity
307            return self
308                .trades
309                .iter()
310                .find(|t| t.direction == OrderSide::Buy)
311                .map_or(Decimal::ZERO, |t| t.quantity);
312        }
313
314        // For very small counts, use scalar sum directly
315        if buy_count <= 3 {
316            return self
317                .iter_by_direction(OrderSide::Buy)
318                .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
319        }
320
321        // For small to medium counts, use stack allocation to avoid heap allocation
322        if buy_count <= 64 {
323            let mut quantities = [Decimal::ZERO; 64];
324
325            for (index, trade) in self.iter_by_direction(OrderSide::Buy).enumerate() {
326                if index >= 64 {
327                    break;
328                }
329                quantities[index] = trade.quantity;
330            }
331
332            // Use SIMD-accelerated sum on the stack-allocated array
333            return SimdOps::sum_decimal(&quantities[..buy_count]);
334        }
335
336        // For larger counts, extract quantities and use SIMD-accelerated sum
337        let quantities: Vec<Decimal> = self
338            .iter_by_direction(OrderSide::Buy)
339            .map(|t| t.quantity)
340            .collect();
341
342        SimdOps::sum_decimal(&quantities)
343    }
344
345    /// Computes the sell volume in the batch
346    ///
347    /// This method is optimized for high-frequency trading using SIMD instructions
348    /// when available. It filters trades by direction and uses SIMD-accelerated
349    /// summation for maximum performance.
350    ///
351    /// # Performance considerations
352    /// - Uses SIMD instructions for parallel processing when available
353    /// - Falls back to scalar operations for small trade counts
354    /// - Avoids heap allocations for small to medium-sized batches
355    /// - Optimized for both small and large trade batches
356    #[inline]
357    #[must_use]
358    pub fn sell_volume(&self) -> Decimal {
359        if self.trades.is_empty() {
360            return Decimal::ZERO;
361        }
362
363        // Count sell trades to optimize allocation strategy
364        let sell_count = self.count_by_direction(OrderSide::Sell);
365
366        if sell_count == 0 {
367            return Decimal::ZERO;
368        }
369
370        if sell_count == 1 {
371            // Find the single sell trade and return its quantity
372            return self
373                .trades
374                .iter()
375                .find(|t| t.direction == OrderSide::Sell)
376                .map_or(Decimal::ZERO, |t| t.quantity);
377        }
378
379        // For very small counts, use scalar sum directly
380        if sell_count <= 3 {
381            return self
382                .iter_by_direction(OrderSide::Sell)
383                .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
384        }
385
386        // For small to medium counts, use stack allocation to avoid heap allocation
387        if sell_count <= 64 {
388            let mut quantities = [Decimal::ZERO; 64];
389
390            for (index, trade) in self.iter_by_direction(OrderSide::Sell).enumerate() {
391                if index >= 64 {
392                    break;
393                }
394                quantities[index] = trade.quantity;
395            }
396
397            // Use SIMD-accelerated sum on the stack-allocated array
398            return SimdOps::sum_decimal(&quantities[..sell_count]);
399        }
400
401        // For larger counts, extract quantities and use SIMD-accelerated sum
402        let quantities: Vec<Decimal> = self
403            .iter_by_direction(OrderSide::Sell)
404            .map(|t| t.quantity)
405            .collect();
406
407        SimdOps::sum_decimal(&quantities)
408    }
409
410    /// Computes the volume imbalance ratio (`buy_volume` - `sell_volume`) / `total_volume`
411    /// Returns a value between -1.0 and 1.0
412    ///
413    /// This method calculates the imbalance between buy and sell volumes, which is a key
414    /// indicator of market pressure. A positive value indicates more buying pressure,
415    /// while a negative value indicates more selling pressure.
416    ///
417    /// # Performance considerations
418    /// - Uses the optimized `buy_volume` and `sell_volume` methods
419    /// - Avoids redundant calculations by computing the total directly
420    /// - Handles edge cases efficiently (empty batch, zero total volume)
421    /// - Uses safe conversion to f64 with fallback
422    ///
423    /// # Returns
424    /// - Some(f64): The volume imbalance ratio between -1.0 and 1.0
425    /// - None: If the batch is empty or the total volume is zero
426    #[inline]
427    #[must_use]
428    pub fn volume_imbalance(&self) -> Option<f64> {
429        if self.trades.is_empty() {
430            return None;
431        }
432
433        // Calculate buy and sell volumes using optimized methods
434        let buy_vol = self.buy_volume();
435        let sell_vol = self.sell_volume();
436
437        // Calculate total volume directly to avoid potential precision issues
438        let total = buy_vol + sell_vol;
439
440        // Check for zero total volume
441        if total.is_zero() {
442            return None;
443        }
444
445        // Calculate imbalance: (buy - sell) / total
446        // This gives a value between -1.0 (all sell) and 1.0 (all buy)
447        let imbalance = (buy_vol - sell_vol) / total;
448
449        // Convert to f64 with fallback to 0.0 if conversion fails
450        Some(rusty_common::decimal_utils::decimal_to_f64_or_nan(
451            imbalance,
452        ))
453    }
454
455    /// Computes the VWAP (Volume Weighted Average Price) for the batch
456    ///
457    /// VWAP is a trading benchmark that gives the average price a security has traded at
458    /// throughout the period, based on both volume and price. It provides insight into
459    /// both the trend and value of a security.
460    ///
461    /// # Performance considerations
462    /// - Uses SIMD-accelerated dot product for price*quantity calculations
463    /// - Uses the optimized `total_volume` method for quantity sum
464    /// - Avoids heap allocations for small to medium-sized batches
465    /// - Handles edge cases efficiently (empty batch, zero total volume)
466    /// - Optimized for both small and large trade batches
467    ///
468    /// # Returns
469    /// - Some(Decimal): The volume-weighted average price
470    /// - None: If the batch is empty or the total volume is zero
471    #[inline]
472    #[must_use]
473    pub fn vwap(&self) -> Option<Decimal> {
474        if self.trades.is_empty() {
475            return None;
476        }
477
478        // Get total quantity using the optimized method
479        let total_quantity = self.total_volume();
480        if total_quantity.is_zero() {
481            return None;
482        }
483
484        if self.trades.len() == 1 {
485            // For a single trade, VWAP is just the price
486            return Some(self.trades[0].price);
487        }
488
489        // For very small batches, use scalar calculation
490        if self.trades.len() <= 3 {
491            let total_price_quantity = self
492                .trades
493                .iter()
494                .fold(Decimal::ZERO, |acc, t| acc + (t.price * t.quantity));
495            // Round to 2 decimal places for consistent financial precision and test compatibility
496            return Some((total_price_quantity / total_quantity).round_dp(2));
497        }
498
499        // For small to medium batches, use stack allocation to avoid heap allocation
500        if self.trades.len() <= 64 {
501            let mut prices = [0.0f64; 64];
502            let mut quantities = [0.0f64; 64];
503
504            for (i, trade) in self.trades.iter().enumerate() {
505                if i >= 64 {
506                    break;
507                }
508                prices[i] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.price);
509                quantities[i] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.quantity);
510            }
511
512            // Use SIMD-accelerated dot product for price*quantity sum
513            let dot_product = SimdOps::dot_product_f64(
514                &prices[..self.trades.len()],
515                &quantities[..self.trades.len()],
516            );
517
518            // Convert back to Decimal and divide by total quantity
519            let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
520            // Round to 2 decimal places for consistent financial precision and test compatibility
521            return Some((total_price_quantity / total_quantity).round_dp(2));
522        }
523
524        // For larger batches, extract prices and quantities and use SIMD-accelerated dot product
525        let prices: Vec<f64> = self
526            .trades
527            .iter()
528            .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.price))
529            .collect();
530
531        let quantities: Vec<f64> = self
532            .trades
533            .iter()
534            .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.quantity))
535            .collect();
536
537        // Calculate dot product (sum of price*quantity for each trade)
538        let dot_product = SimdOps::dot_product_f64(&prices, &quantities);
539
540        // Convert back to Decimal and calculate VWAP
541        let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
542        // Round to 2 decimal places for consistent financial precision and test compatibility
543        Some((total_price_quantity / total_quantity).round_dp(2))
544    }
545
546    /// Computes the VWAP for a specific direction (buy or sell)
547    ///
548    /// This method calculates the Volume Weighted Average Price for trades in a specific
549    /// direction (buy or sell). This is useful for analyzing price trends separately
550    /// for buys and sells.
551    ///
552    /// # Performance considerations
553    /// - Uses SIMD-accelerated dot product for price*quantity calculations
554    /// - Filters trades by direction efficiently
555    /// - Avoids heap allocations for small to medium-sized batches
556    /// - Handles edge cases efficiently (empty batch, zero total volume)
557    /// - Optimized for both small and large trade batches
558    ///
559    /// # Parameters
560    /// - `direction`: The trade direction to calculate VWAP for (Buy or Sell)
561    ///
562    /// # Returns
563    /// - Some(Decimal): The volume-weighted average price for the specified direction
564    /// - None: If there are no trades in the specified direction or the total volume is zero
565    #[inline]
566    #[must_use]
567    pub fn vwap_by_direction(&self, direction: OrderSide) -> Option<Decimal> {
568        if self.trades.is_empty() {
569            return None;
570        }
571
572        // Count trades in the specified direction to optimize allocation strategy
573        let count = self.count_by_direction(direction);
574
575        if count == 0 {
576            return None;
577        }
578
579        if count == 1 {
580            // Find the single trade in the specified direction and return its price
581            return self
582                .trades
583                .iter()
584                .find(|t| t.direction == direction)
585                .map(|t| t.price);
586        }
587
588        // For very small counts, use scalar calculation
589        if count <= 3 {
590            let mut total_quantity = Decimal::ZERO;
591            let mut total_price_quantity = Decimal::ZERO;
592
593            for trade in self.iter_by_direction(direction) {
594                total_quantity += trade.quantity;
595                total_price_quantity += trade.price * trade.quantity;
596            }
597
598            if total_quantity.is_zero() {
599                return None;
600            }
601
602            // Round to 2 decimal places for consistent financial precision and test compatibility
603            return Some((total_price_quantity / total_quantity).round_dp(2));
604        }
605
606        // For small to medium counts, use stack allocation to avoid heap allocation
607        if count <= 64 {
608            let mut prices = [0.0f64; 64];
609            let mut quantities = [0.0f64; 64];
610            let mut total_quantity = Decimal::ZERO;
611
612            for (index, trade) in self.iter_by_direction(direction).enumerate() {
613                if index >= 64 {
614                    break;
615                }
616                prices[index] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.price);
617                quantities[index] =
618                    rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.quantity);
619                total_quantity += trade.quantity;
620            }
621
622            if total_quantity.is_zero() {
623                return None;
624            }
625
626            // Use SIMD-accelerated dot product for price*quantity sum
627            let dot_product = SimdOps::dot_product_f64(&prices[..count], &quantities[..count]);
628
629            // Convert back to Decimal and divide by total quantity
630            let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
631            // Round to 2 decimal places for consistent financial precision and test compatibility
632            return Some((total_price_quantity / total_quantity).round_dp(2));
633        }
634
635        // For larger counts, collect filtered trades and use SIMD-accelerated dot product
636        let filtered_trades: Vec<&MarketTrade> = self.iter_by_direction(direction).collect();
637
638        let prices: Vec<f64> = filtered_trades
639            .iter()
640            .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.price))
641            .collect();
642
643        let quantities: Vec<f64> = filtered_trades
644            .iter()
645            .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.quantity))
646            .collect();
647
648        // Calculate total quantity (we need this in Decimal precision)
649        let total_quantity = filtered_trades
650            .iter()
651            .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
652
653        if total_quantity.is_zero() {
654            return None;
655        }
656
657        // Calculate dot product (sum of price*quantity for each trade)
658        let dot_product = SimdOps::dot_product_f64(&prices, &quantities);
659
660        // Convert back to Decimal and calculate VWAP
661        let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
662        // Round to 2 decimal places for consistent financial precision and test compatibility
663        Some((total_price_quantity / total_quantity).round_dp(2))
664    }
665
666    /// Clear all trades but retain allocated memory
667    #[inline]
668    pub fn clear(&mut self) {
669        self.trades.clear();
670        self.last_update = self.clock.raw();
671    }
672
673    /// Truncate the batch to the specified size, keeping only the most recent trades
674    ///
675    /// This method ensures that the batch contains at most `size` trades, removing
676    /// older trades if necessary. Since trades are inserted in chronological order,
677    /// this effectively creates a sliding window of the most recent trades.
678    ///
679    /// # Performance considerations
680    /// - Uses an optimized approach to minimize memory operations
681    /// - For small batches, uses the standard drain method
682    /// - For large batches, creates a new `SmallVec` with the most recent trades
683    /// - Updates the `last_update` timestamp atomically
684    ///
685    /// # Parameters
686    /// - `size`: The maximum number of trades to keep in the batch
687    #[inline]
688    pub fn truncate(&mut self, size: usize) {
689        if self.trades.len() <= size {
690            // Nothing to truncate
691            return;
692        }
693
694        let current_len = self.trades.len();
695        let start_idx = current_len - size;
696
697        // For small batches or small truncations, use drain which is efficient enough
698        if current_len <= 128 || start_idx <= 32 {
699            self.trades.drain(0..start_idx);
700        } else {
701            // For large batches with significant truncation, it's more efficient
702            // to create a new SmallVec with just the trades we want to keep
703            let mut new_trades = SmallVec::with_capacity(size);
704
705            // Move the most recent trades to the new SmallVec
706            // This avoids shifting all elements in the original vector
707            for i in start_idx..current_len {
708                new_trades.push(self.trades[i].clone());
709            }
710
711            // Replace the old trades with the new ones
712            self.trades = new_trades;
713        }
714
715        // Update the last_update timestamp
716        self.last_update = self.clock.raw();
717    }
718
719    /// Get the most recent trade
720    #[inline]
721    #[must_use]
722    pub fn latest_trade(&self) -> Option<&MarketTrade> {
723        self.trades.last()
724    }
725
726    /// Calculate the average trade size
727    #[inline]
728    #[must_use]
729    pub fn average_trade_size(&self) -> Option<Decimal> {
730        if self.trades.is_empty() {
731            return None;
732        }
733
734        Some(self.total_volume() / Decimal::from(self.trades.len()))
735    }
736
737    /// Calculate the median trade price
738    ///
739    /// The median price is the middle value when all prices are sorted. For an even
740    /// number of trades, it's the average of the two middle values. This is a robust
741    /// measure of central tendency that is less affected by outliers than the mean.
742    ///
743    /// # Performance considerations
744    /// - Uses an optimized approach based on the number of trades
745    /// - For small batches, uses a simple sort-based approach
746    /// - For larger batches, uses a partial sort to find the median efficiently
747    /// - Avoids unnecessary allocations and copies where possible
748    /// - Handles edge cases efficiently (empty batch, single trade)
749    ///
750    /// # Returns
751    /// - Some(Decimal): The median price
752    /// - None: If the batch is empty
753    #[inline]
754    #[must_use]
755    pub fn median_price(&self) -> Option<Decimal> {
756        if self.trades.is_empty() {
757            return None;
758        }
759
760        if self.trades.len() == 1 {
761            return Some(self.trades[0].price);
762        }
763
764        // For very small batches, use a simple approach
765        if self.trades.len() <= 5 {
766            let mut prices = [Decimal::ZERO; 5];
767            for (i, trade) in self.trades.iter().enumerate() {
768                prices[i] = trade.price;
769            }
770
771            // Sort the small array
772            prices[..self.trades.len()].sort();
773
774            let mid = self.trades.len() / 2;
775            if self.trades.len().is_multiple_of(2) {
776                // Even number of elements, take average of middle two
777                return Some((prices[mid - 1] + prices[mid]) / Decimal::TWO);
778            }
779            // Odd number of elements, take the middle one
780            return Some(prices[mid]);
781        }
782
783        // For small to medium batches, use a standard sort-based approach
784        if self.trades.len() <= 64 {
785            let mut prices = [Decimal::ZERO; 64];
786            for (i, trade) in self.trades.iter().enumerate() {
787                if i >= 64 {
788                    break;
789                }
790                prices[i] = trade.price;
791            }
792
793            // Sort the array
794            prices[..self.trades.len()].sort();
795
796            let mid = self.trades.len() / 2;
797            if self.trades.len().is_multiple_of(2) {
798                // Even number of elements, take average of middle two
799                return Some((prices[mid - 1] + prices[mid]) / Decimal::TWO);
800            }
801            // Odd number of elements, take the middle one
802            return Some(prices[mid]);
803        }
804
805        // For larger batches, use a heap-allocated vector
806        // This is used rarely enough that the allocation is acceptable
807        let mut prices: Vec<Decimal> = self.trades.iter().map(|t| t.price).collect();
808
809        // Find the median position(s)
810        let mid = prices.len() / 2;
811
812        if prices.len().is_multiple_of(2) {
813            // Even number of elements, need to find the two middle elements
814            // Use partial_sort to avoid sorting the entire array
815            let (left, _, _) = prices.select_nth_unstable(mid - 1);
816            let median_left = left[mid - 1];
817
818            let (_, median_right, _) = prices.select_nth_unstable(mid);
819
820            // Return the average of the two middle elements
821            Some((median_left + *median_right) / Decimal::TWO)
822        } else {
823            // Odd number of elements, just need to find the middle element
824            // Use select_nth_unstable which is more efficient than sorting
825            let (_, median, _) = prices.select_nth_unstable(mid);
826            Some(*median)
827        }
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834    use crate::venues::Venue;
835    use rust_decimal_macros::dec;
836    use std::thread;
837    use std::time::Duration;
838
839    fn create_instrument_id() -> InstrumentId {
840        InstrumentId::new("BTCUSDT", Venue::Binance)
841    }
842
843    #[test]
844    fn test_add_trade() {
845        let mut batch = TradeBatch::new(create_instrument_id());
846
847        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
848        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
849
850        assert_eq!(batch.len(), 2);
851        assert_eq!(batch.trades()[0].price, dec!(100.5));
852        assert_eq!(batch.trades()[0].quantity, dec!(10.0));
853        assert_eq!(batch.trades()[0].direction, OrderSide::Buy);
854        assert_eq!(batch.trades()[1].price, dec!(101.0));
855        assert_eq!(batch.trades()[1].quantity, dec!(5.0));
856        assert_eq!(batch.trades()[1].direction, OrderSide::Sell);
857    }
858
859    #[test]
860    fn test_count_by_direction() {
861        let mut batch = TradeBatch::new(create_instrument_id());
862
863        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
864        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
865        batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
866
867        assert_eq!(batch.count_by_direction(OrderSide::Buy), 2);
868        assert_eq!(batch.count_by_direction(OrderSide::Sell), 1);
869    }
870
871    #[test]
872    fn test_total_volume() {
873        let mut batch = TradeBatch::new(create_instrument_id());
874
875        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
876        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
877        batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
878
879        let total_volume = batch.total_volume();
880        assert_eq!(total_volume, dec!(22.5));
881    }
882
883    #[test]
884    fn test_vwap() {
885        let mut batch = TradeBatch::new(create_instrument_id());
886
887        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
888        batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
889
890        let vwap = batch.vwap();
891        assert!(vwap.is_some());
892        assert_eq!(vwap.unwrap(), dec!(100.67));
893    }
894
895    #[test]
896    fn test_vwap_with_no_trades() {
897        let batch = TradeBatch::new(create_instrument_id());
898
899        let vwap = batch.vwap();
900        assert!(vwap.is_none());
901    }
902
903    #[test]
904    fn test_iter_after() {
905        let clock = Clock::new();
906        let mut batch = TradeBatch::with_clock(create_instrument_id(), clock.clone());
907
908        let timestamp1 = clock.now();
909        thread::sleep(Duration::from_millis(10));
910        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
911        let timestamp2 = clock.now();
912        thread::sleep(Duration::from_millis(10));
913        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
914
915        assert_eq!(batch.iter_after(timestamp1).count(), 2);
916
917        let filtered_trades: Vec<_> = batch.iter_after(timestamp2).collect();
918        assert_eq!(filtered_trades.len(), 1);
919        assert_eq!(filtered_trades[0].price, dec!(101.0));
920    }
921
922    #[test]
923    fn test_iter_before() {
924        let clock = Clock::new();
925        let mut batch = TradeBatch::with_clock(create_instrument_id(), clock.clone());
926
927        let timestamp1 = clock.now();
928        thread::sleep(Duration::from_millis(10));
929        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
930        let timestamp2 = clock.now();
931        thread::sleep(Duration::from_millis(10));
932        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
933
934        assert_eq!(batch.iter_before(timestamp1).count(), 0);
935
936        let filtered_trades: Vec<_> = batch.iter_before(timestamp2).collect();
937        assert_eq!(filtered_trades.len(), 1);
938        assert_eq!(filtered_trades[0].price, dec!(100.5));
939    }
940
941    #[test]
942    fn test_clear() {
943        let mut batch = TradeBatch::new(create_instrument_id());
944
945        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
946        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
947
948        assert_eq!(batch.len(), 2);
949
950        batch.clear();
951
952        assert_eq!(batch.len(), 0);
953    }
954
955    #[test]
956    fn test_truncate() {
957        let mut batch = TradeBatch::new(create_instrument_id());
958
959        batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
960        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
961        batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
962
963        assert_eq!(batch.len(), 3);
964
965        batch.truncate(2);
966
967        assert_eq!(batch.len(), 2);
968        assert_eq!(batch.trades()[0].price, dec!(101.0));
969        assert_eq!(batch.trades()[1].price, dec!(102.5));
970    }
971
972    #[test]
973    fn test_volume_imbalance() {
974        let mut batch = TradeBatch::new(create_instrument_id());
975
976        // Equal buy and sell volume
977        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
978        batch.add_trade(dec!(102.0), dec!(10.0), OrderSide::Sell, 0);
979
980        let imbalance = batch.volume_imbalance();
981        assert!(imbalance.is_some());
982        assert!((imbalance.unwrap() - 0.0).abs() < f64::EPSILON);
983
984        // Clear and add more buy volume
985        batch.clear();
986        batch.add_trade(dec!(100.0), dec!(15.0), OrderSide::Buy, 0);
987        batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
988
989        let imbalance = batch.volume_imbalance();
990        assert!(imbalance.is_some());
991        // (15 - 5) / 20 = 0.5
992        assert!((imbalance.unwrap() - 0.5).abs() < f64::EPSILON);
993    }
994
995    #[test]
996    fn test_buy_sell_volume() {
997        let mut batch = TradeBatch::new(create_instrument_id());
998
999        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1000        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1001        batch.add_trade(dec!(102.0), dec!(7.5), OrderSide::Sell, 0);
1002
1003        assert_eq!(batch.buy_volume(), dec!(15.0));
1004        assert_eq!(batch.sell_volume(), dec!(7.5));
1005    }
1006
1007    #[test]
1008    fn test_vwap_by_direction() {
1009        let mut batch = TradeBatch::new(create_instrument_id());
1010
1011        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1012        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1013        batch.add_trade(dec!(102.0), dec!(10.0), OrderSide::Sell, 0);
1014
1015        // Buy VWAP: (100 * 10 + 101 * 5) / 15 = 100.33
1016        let buy_vwap = batch.vwap_by_direction(OrderSide::Buy);
1017        assert!(buy_vwap.is_some());
1018        assert_eq!(buy_vwap.unwrap().round_dp(2), dec!(100.33));
1019
1020        // Sell VWAP: 102
1021        let sell_vwap = batch.vwap_by_direction(OrderSide::Sell);
1022        assert!(sell_vwap.is_some());
1023        assert_eq!(sell_vwap.unwrap(), dec!(102.0));
1024    }
1025
1026    #[test]
1027    fn test_latest_trade() {
1028        let mut batch = TradeBatch::new(create_instrument_id());
1029
1030        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1031        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1032
1033        let latest = batch.latest_trade();
1034        assert!(latest.is_some());
1035        assert_eq!(latest.unwrap().price, dec!(101.0));
1036    }
1037
1038    #[test]
1039    fn test_average_trade_size() {
1040        let mut batch = TradeBatch::new(create_instrument_id());
1041
1042        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1043        batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
1044
1045        let avg_size = batch.average_trade_size();
1046        assert!(avg_size.is_some());
1047        assert_eq!(avg_size.unwrap(), dec!(7.5));
1048    }
1049
1050    #[test]
1051    fn test_trade_notional_value() {
1052        let clock = Clock::new();
1053        let instrument_id = create_instrument_id();
1054
1055        let trade = MarketTrade::new(
1056            dec!(100.5),
1057            dec!(10.0),
1058            OrderSide::Buy,
1059            instrument_id,
1060            &clock,
1061            0,
1062        );
1063
1064        assert_eq!(trade.notional_value(), dec!(1005.0));
1065    }
1066
1067    #[test]
1068    fn test_trade_latency() {
1069        let clock = Clock::new();
1070        let instrument_id = create_instrument_id();
1071
1072        // Test with exchange_time_ns = 0 (should return None)
1073        let trade1 = MarketTrade::new(
1074            dec!(100.0),
1075            dec!(10.0),
1076            OrderSide::Buy,
1077            instrument_id.clone(),
1078            &clock,
1079            0,
1080        );
1081        assert_eq!(trade1.latency(), None);
1082
1083        // Test with non-zero exchange_time_ns
1084        // Since we can't predict the exact latency in a test, we'll just verify it returns Some
1085        let now_ns = clock.raw();
1086        let trade2 = MarketTrade::new(
1087            dec!(100.0),
1088            dec!(10.0),
1089            OrderSide::Buy,
1090            instrument_id,
1091            &clock,
1092            now_ns - 1_000_000, // 1ms earlier
1093        );
1094        assert!(trade2.latency().is_some());
1095    }
1096
1097    #[test]
1098    fn test_median_price() {
1099        let mut batch = TradeBatch::new(create_instrument_id());
1100
1101        // Test with empty batch
1102        assert_eq!(batch.median_price(), None);
1103
1104        // Test with odd number of trades
1105        batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1106        batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
1107        batch.add_trade(dec!(101.0), dec!(7.5), OrderSide::Buy, 0);
1108
1109        let median = batch.median_price();
1110        assert!(median.is_some());
1111        assert_eq!(median.unwrap(), dec!(101.0));
1112
1113        // Test with even number of trades
1114        batch.add_trade(dec!(103.0), dec!(3.0), OrderSide::Sell, 0);
1115
1116        let median = batch.median_price();
1117        assert!(median.is_some());
1118        assert_eq!(median.unwrap(), dec!(101.5)); // (101 + 102) / 2 = 101.5
1119    }
1120}