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}