drm_core/utils/
price.rs

1//! Price utility functions for tick size rounding and validation.
2
3use crate::error::DrmError;
4
5/// Round a price to the nearest valid tick increment.
6///
7/// # Arguments
8/// * `price` - The price to round
9/// * `tick_size` - The minimum tick size
10///
11/// # Returns
12/// Price rounded to nearest tick
13///
14/// # Example
15/// ```
16/// use drm_core::utils::round_to_tick_size;
17///
18/// let rounded = round_to_tick_size(0.1234, 0.01).unwrap();
19/// assert!((rounded - 0.12).abs() < 1e-10);
20/// ```
21pub fn round_to_tick_size(price: f64, tick_size: f64) -> Result<f64, DrmError> {
22    if tick_size <= 0.0 {
23        return Err(DrmError::InvalidInput(
24            "tick_size must be positive".to_string(),
25        ));
26    }
27
28    Ok((price / tick_size).round() * tick_size)
29}
30
31/// Check if a price is valid for the given tick size.
32///
33/// # Arguments
34/// * `price` - Price to check
35/// * `tick_size` - Minimum tick size
36///
37/// # Returns
38/// True if price is valid (aligned to tick size)
39///
40/// # Example
41/// ```
42/// use drm_core::utils::is_valid_price;
43///
44/// assert!(is_valid_price(0.12, 0.01).unwrap());
45/// assert!(!is_valid_price(0.123, 0.01).unwrap());
46/// ```
47pub fn is_valid_price(price: f64, tick_size: f64) -> Result<bool, DrmError> {
48    if tick_size <= 0.0 {
49        return Err(DrmError::InvalidInput(
50            "tick_size must be positive".to_string(),
51        ));
52    }
53
54    let rounded = round_to_tick_size(price, tick_size)?;
55    Ok((price - rounded).abs() < (tick_size / 10.0))
56}
57
58/// Clamp a price to be within valid bounds.
59///
60/// # Arguments
61/// * `price` - Price to clamp
62/// * `min_price` - Minimum allowed price
63/// * `max_price` - Maximum allowed price
64/// * `tick_size` - Tick size to round to
65///
66/// # Returns
67/// Price clamped to bounds and rounded to tick size
68pub fn clamp_price(
69    price: f64,
70    min_price: f64,
71    max_price: f64,
72    tick_size: f64,
73) -> Result<f64, DrmError> {
74    let clamped = price.clamp(min_price, max_price);
75    round_to_tick_size(clamped, tick_size)
76}
77
78/// Calculate mid price from best bid and ask.
79///
80/// # Arguments
81/// * `best_bid` - Best bid price
82/// * `best_ask` - Best ask price
83///
84/// # Returns
85/// Mid price, or None if either price is missing
86pub fn mid_price(best_bid: Option<f64>, best_ask: Option<f64>) -> Option<f64> {
87    match (best_bid, best_ask) {
88        (Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
89        _ => None,
90    }
91}
92
93/// Calculate spread in basis points.
94///
95/// # Arguments
96/// * `best_bid` - Best bid price
97/// * `best_ask` - Best ask price
98///
99/// # Returns
100/// Spread in basis points, or None if either price is missing
101pub fn spread_bps(best_bid: Option<f64>, best_ask: Option<f64>) -> Option<f64> {
102    match (best_bid, best_ask) {
103        (Some(bid), Some(ask)) if bid > 0.0 => {
104            let mid = (bid + ask) / 2.0;
105            Some((ask - bid) / mid * 10000.0)
106        }
107        _ => None,
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_round_to_tick_size() {
117        assert!((round_to_tick_size(0.1234, 0.01).unwrap() - 0.12).abs() < 1e-10);
118        assert!((round_to_tick_size(0.1256, 0.01).unwrap() - 0.13).abs() < 1e-10);
119        assert!((round_to_tick_size(0.5, 0.1).unwrap() - 0.5).abs() < 1e-10);
120        assert!((round_to_tick_size(0.55, 0.1).unwrap() - 0.6).abs() < 1e-10);
121    }
122
123    #[test]
124    fn test_round_to_tick_size_invalid() {
125        assert!(round_to_tick_size(0.5, 0.0).is_err());
126        assert!(round_to_tick_size(0.5, -0.01).is_err());
127    }
128
129    #[test]
130    fn test_is_valid_price() {
131        assert!(is_valid_price(0.12, 0.01).unwrap());
132        assert!(is_valid_price(0.50, 0.01).unwrap());
133        assert!(!is_valid_price(0.123, 0.01).unwrap());
134        assert!(!is_valid_price(0.1234, 0.01).unwrap());
135    }
136
137    #[test]
138    fn test_clamp_price() {
139        assert!((clamp_price(0.15, 0.10, 0.90, 0.01).unwrap() - 0.15).abs() < 1e-10);
140        assert!((clamp_price(0.05, 0.10, 0.90, 0.01).unwrap() - 0.10).abs() < 1e-10);
141        assert!((clamp_price(0.95, 0.10, 0.90, 0.01).unwrap() - 0.90).abs() < 1e-10);
142    }
143
144    #[test]
145    fn test_mid_price() {
146        assert!((mid_price(Some(0.40), Some(0.60)).unwrap() - 0.50).abs() < 1e-10);
147        assert!(mid_price(None, Some(0.60)).is_none());
148        assert!(mid_price(Some(0.40), None).is_none());
149    }
150
151    #[test]
152    fn test_spread_bps() {
153        // Spread = 0.60 - 0.40 = 0.20, Mid = 0.50
154        // BPS = 0.20 / 0.50 * 10000 = 4000
155        let spread = spread_bps(Some(0.40), Some(0.60)).unwrap();
156        assert!((spread - 4000.0).abs() < 1e-10);
157    }
158}