Skip to main content

brokapi_bcs_protocol/utils/
precision.rs

1//! Price and quantity arithmetic in whole steps, exactly.
2//!
3//! Exchange prices move in steps (for example 0.05) and an order price must sit on the step grid, or
4//! the exchange refuses it. The numbers are [`Decimal`]s, so the step arithmetic has no noise:
5//! `101.05 / 0.05` is exactly `2021` (in `f64` it is `2020.9999999999998`). The functions here count
6//! the value in whole steps and give it back. The step comes from the instrument data, see
7//! [`TradingParamsSnapshot::price_step`](crate::http::models::TradingParamsSnapshot::price_step).
8//!
9//! All functions are pure and return a [`PrecisionError`] for a step that cannot be used. The grid
10//! starts at zero.
11//!
12//! # Examples
13//!
14//! ```
15//! use brokapi_bcs_protocol::utils::precision::{precise_down, precise_up, precise_to_nearest};
16//! use rust_decimal::Decimal;
17//!
18//! let price = Decimal::new(10103, 2); // 101.03
19//! let step = Decimal::new(5, 2); // 0.05
20//! // A buy price is rounded down, a sell price up, so you never pay or sell worse than computed.
21//! assert_eq!(precise_down(price, step)?, Decimal::new(10100, 2));
22//! assert_eq!(precise_up(price, step)?, Decimal::new(10105, 2));
23//! assert_eq!(precise_to_nearest(price, step)?, Decimal::new(10105, 2));
24//! # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
25//! ```
26
27use rust_decimal::Decimal;
28use thiserror::Error;
29
30/// Why a step calculation was refused.
31///
32/// # Examples
33///
34/// ```
35/// use brokapi_bcs_protocol::utils::precision::{precise_down, PrecisionError};
36/// use rust_decimal::Decimal;
37///
38/// assert!(matches!(
39///     precise_down(Decimal::ONE_HUNDRED, Decimal::ZERO),
40///     Err(PrecisionError::InvalidStep(_))
41/// ));
42/// ```
43#[derive(Error, Debug, Clone, PartialEq, Eq)]
44pub enum PrecisionError {
45    /// The step is zero or negative.
46    #[error("step must be above 0, got {0}")]
47    InvalidStep(Decimal),
48
49    /// The count of steps does not fit in an `i64`, or a number overflowed.
50    #[error("steps overflow: value={value}, step={step}")]
51    StepsOverflow {
52        /// The value that was converted.
53        value: Decimal,
54        /// The step it was divided by.
55        step: Decimal,
56    },
57}
58
59fn validate_step(step: Decimal) -> Result<(), PrecisionError> {
60    if step <= Decimal::ZERO {
61        return Err(PrecisionError::InvalidStep(step));
62    }
63    Ok(())
64}
65
66#[derive(Clone, Copy)]
67enum Mode {
68    Down,
69    Up,
70    Nearest,
71}
72
73fn steps(value: Decimal, step: Decimal, mode: Mode) -> Result<i64, PrecisionError> {
74    validate_step(step)?;
75    let overflow = || PrecisionError::StepsOverflow { value, step };
76    // The remainder is exact, so a value on the grid is recognised as one.
77    let rem = value.checked_rem(step).ok_or_else(overflow)?;
78    let whole = value.checked_sub(rem).ok_or_else(overflow)?;
79    // `whole` is a multiple of step with the sign of value; the count of steps is exact.
80    let mut n = whole
81        .checked_div(step)
82        .and_then(|d| i64::try_from(d.trunc()).ok())
83        .ok_or_else(overflow)?;
84    if rem.is_zero() {
85        return Ok(n);
86    }
87    // n is now toward zero. Move to the floor or the ceiling.
88    let negative = rem < Decimal::ZERO;
89    let floor = if negative { n - 1 } else { n };
90    let ceil = floor + 1;
91    n = match mode {
92        Mode::Down => floor,
93        Mode::Up => ceil,
94        Mode::Nearest => {
95            // Halves go away from zero.
96            let twice = rem.abs() * Decimal::TWO;
97            if twice >= step {
98                if negative { floor } else { ceil }
99            } else if negative {
100                ceil
101            } else {
102                floor
103            }
104        }
105    };
106    Ok(n)
107}
108
109/// Converts a value to a whole number of steps, rounding to the **nearest** step (halves go away from
110/// zero).
111///
112/// # Errors
113///
114/// [`PrecisionError::InvalidStep`] for a step that is zero or negative,
115/// [`PrecisionError::StepsOverflow`] if the count does not fit in an `i64`.
116///
117/// # Examples
118///
119/// ```
120/// use brokapi_bcs_protocol::utils::precision::value_to_steps_nearest;
121/// use rust_decimal::Decimal;
122///
123/// assert_eq!(value_to_steps_nearest(Decimal::new(10105, 2), Decimal::new(5, 2))?, 2021);
124/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
125/// ```
126pub fn value_to_steps_nearest(value: Decimal, step: Decimal) -> Result<i64, PrecisionError> {
127    steps(value, step, Mode::Nearest)
128}
129
130/// Converts a value to a whole number of steps, rounding **down** (towards negative infinity).
131///
132/// # Errors
133///
134/// The same as [`value_to_steps_nearest`].
135///
136/// # Examples
137///
138/// ```
139/// use brokapi_bcs_protocol::utils::precision::value_to_steps_down;
140/// use rust_decimal::Decimal;
141///
142/// assert_eq!(value_to_steps_down(Decimal::new(10103, 2), Decimal::new(5, 2))?, 2020);
143/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
144/// ```
145pub fn value_to_steps_down(value: Decimal, step: Decimal) -> Result<i64, PrecisionError> {
146    steps(value, step, Mode::Down)
147}
148
149/// Converts a value to a whole number of steps, rounding **up** (towards positive infinity).
150///
151/// # Errors
152///
153/// The same as [`value_to_steps_nearest`].
154///
155/// # Examples
156///
157/// ```
158/// use brokapi_bcs_protocol::utils::precision::value_to_steps_up;
159/// use rust_decimal::Decimal;
160///
161/// assert_eq!(value_to_steps_up(Decimal::new(10103, 2), Decimal::new(5, 2))?, 2021);
162/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
163/// ```
164pub fn value_to_steps_up(value: Decimal, step: Decimal) -> Result<i64, PrecisionError> {
165    steps(value, step, Mode::Up)
166}
167
168/// Converts a whole number of steps back to a value, exactly: `2001` steps of `0.05` is `100.05`.
169///
170/// # Errors
171///
172/// [`PrecisionError::InvalidStep`] if the step is zero or negative, [`PrecisionError::StepsOverflow`]
173/// if the product does not fit.
174///
175/// # Examples
176///
177/// ```
178/// use brokapi_bcs_protocol::utils::precision::steps_to_value;
179/// use rust_decimal::Decimal;
180///
181/// assert_eq!(steps_to_value(2001, Decimal::new(5, 2))?, Decimal::new(10005, 2));
182/// assert_eq!(steps_to_value(3, Decimal::new(1, 3))?, Decimal::new(3, 3));
183/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
184/// ```
185pub fn steps_to_value(steps: i64, step: Decimal) -> Result<Decimal, PrecisionError> {
186    validate_step(step)?;
187    Decimal::from(steps)
188        .checked_mul(step)
189        .ok_or(PrecisionError::StepsOverflow {
190            value: Decimal::from(steps),
191            step,
192        })
193}
194
195/// Rounds a value to the **nearest** point of the step grid.
196///
197/// # Errors
198///
199/// The same as [`value_to_steps_nearest`].
200///
201/// # Examples
202///
203/// ```
204/// use brokapi_bcs_protocol::utils::precision::precise_to_nearest;
205/// use rust_decimal::Decimal;
206///
207/// let step = Decimal::new(5, 2);
208/// assert_eq!(precise_to_nearest(Decimal::new(10102, 2), step)?, Decimal::new(10100, 2));
209/// assert_eq!(precise_to_nearest(Decimal::new(10103, 2), step)?, Decimal::new(10105, 2));
210/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
211/// ```
212pub fn precise_to_nearest(value: Decimal, step: Decimal) -> Result<Decimal, PrecisionError> {
213    steps_to_value(value_to_steps_nearest(value, step)?, step)
214}
215
216/// Rounds a value **down** to the step grid.
217///
218/// Typical for a **buy** price: never pay more than the computed value.
219///
220/// # Errors
221///
222/// The same as [`value_to_steps_nearest`].
223///
224/// # Examples
225///
226/// ```
227/// use brokapi_bcs_protocol::utils::precision::precise_down;
228/// use rust_decimal::Decimal;
229///
230/// assert_eq!(precise_down(Decimal::new(10103, 2), Decimal::new(5, 2))?, Decimal::new(10100, 2));
231/// // a whole-number step works too
232/// assert_eq!(precise_down(Decimal::new(143, 1), Decimal::from(5))?, Decimal::from(10));
233/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
234/// ```
235pub fn precise_down(value: Decimal, step: Decimal) -> Result<Decimal, PrecisionError> {
236    steps_to_value(value_to_steps_down(value, step)?, step)
237}
238
239/// Rounds a value **up** to the step grid.
240///
241/// Typical for a **sell** price: never sell below the computed value.
242///
243/// # Errors
244///
245/// The same as [`value_to_steps_nearest`].
246///
247/// # Examples
248///
249/// ```
250/// use brokapi_bcs_protocol::utils::precision::precise_up;
251/// use rust_decimal::Decimal;
252///
253/// let step = Decimal::new(5, 2);
254/// assert_eq!(precise_up(Decimal::new(10103, 2), step)?, Decimal::new(10105, 2));
255/// // already on the grid: unchanged
256/// assert_eq!(precise_up(Decimal::new(10105, 2), step)?, Decimal::new(10105, 2));
257/// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
258/// ```
259pub fn precise_up(value: Decimal, step: Decimal) -> Result<Decimal, PrecisionError> {
260    steps_to_value(value_to_steps_up(value, step)?, step)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use proptest::prelude::*;
267
268    fn d(s: &str) -> Decimal {
269        s.parse().unwrap()
270    }
271
272    #[test]
273    fn test_quantize_nearest_basic() {
274        assert_eq!(
275            precise_to_nearest(d("101.03"), d("0.05")).unwrap(),
276            d("101.05")
277        );
278        assert_eq!(
279            precise_to_nearest(d("101.02"), d("0.05")).unwrap(),
280            d("101.00")
281        );
282        assert_eq!(
283            precise_to_nearest(d("101.025"), d("0.05")).unwrap(),
284            d("101.05")
285        );
286        assert_eq!(
287            precise_to_nearest(d("-101.025"), d("0.05")).unwrap(),
288            d("-101.05")
289        );
290    }
291
292    #[test]
293    fn test_quantize_down_no_overpay() {
294        assert_eq!(precise_down(d("101.03"), d("0.05")).unwrap(), d("101.00"));
295        assert_eq!(precise_down(d("101.05"), d("0.05")).unwrap(), d("101.05"));
296        assert_eq!(precise_down(d("-101.03"), d("0.05")).unwrap(), d("-101.05"));
297    }
298
299    #[test]
300    fn test_quantize_up_no_undersell() {
301        assert_eq!(precise_up(d("101.03"), d("0.05")).unwrap(), d("101.05"));
302        assert_eq!(precise_up(d("101.05"), d("0.05")).unwrap(), d("101.05"));
303        assert_eq!(precise_up(d("-101.03"), d("0.05")).unwrap(), d("-101.00"));
304    }
305
306    #[test]
307    fn test_steps_to_value_is_exact() {
308        assert_eq!(steps_to_value(2001, d("0.05")).unwrap(), d("100.05"));
309        assert_eq!(steps_to_value(3, d("0.001")).unwrap(), d("0.003"));
310        // The f64 ratios that were off by a hair are whole steps here.
311        assert_eq!(value_to_steps_down(d("101.05"), d("0.05")).unwrap(), 2021);
312        assert_eq!(value_to_steps_down(d("244.3"), d("0.01")).unwrap(), 24430);
313    }
314
315    #[test]
316    fn test_invalid_step() {
317        for bad in ["0", "-0.05"] {
318            assert!(matches!(
319                precise_to_nearest(d("100"), d(bad)),
320                Err(PrecisionError::InvalidStep(_))
321            ));
322        }
323    }
324
325    #[test]
326    fn test_integer_step() {
327        assert_eq!(precise_down(d("14.3"), d("5")).unwrap(), d("10"));
328        assert_eq!(precise_up(d("11"), d("5")).unwrap(), d("15"));
329        assert_eq!(precise_to_nearest(d("12.5"), d("5")).unwrap(), d("15"));
330    }
331
332    proptest! {
333        #[test]
334        fn grid_brackets_the_value(m in -1_000_000_000_i64..1_000_000_000, s in 0u32..=6, sm in 1_i64..10_000, ss in 0u32..=6) {
335            let v = Decimal::new(m, s);
336            let step = Decimal::new(sm, ss);
337            let down = precise_down(v, step).unwrap();
338            let up = precise_up(v, step).unwrap();
339            let near = precise_to_nearest(v, step).unwrap();
340            prop_assert!(down <= v && v <= up && up - down <= step);
341            prop_assert!(near == down || near == up);
342            prop_assert!(down.checked_rem(step).unwrap().is_zero());
343        }
344    }
345}