bothan_lib/registry/post_processor/
tick.rs

1// ! Tick value conversion post-processor.
2//!
3//! This module provides a post-processor that converts decimal values into
4//! tick values based on a logarithmic scale. Tick values represent price points
5//! on a non-linear scale, which can be useful for certain financial applications.
6//!
7//! The module provides:
8//!
9//! - The [`TickPostProcessor`] struct which implements conversion to tick values
10//! - Constants defining the tick scale parameters
11//! - Error handling for out-of-bound conversions
12//!
13//! # Tick System
14//!
15//! The tick system uses a logarithmic scale with base 1.0001, where:
16//!
17//! - Tick 262144 (MID_TICK) corresponds to a value of 1.0
18//! - Higher tick values represent larger numbers
19//! - Lower tick values represent smaller numbers
20//! - The valid range is from tick 1 to tick 524287
21//!
22//! This provides a compact representation for a wide range of values with
23//! consistent relative precision.
24
25use bincode::{Decode, Encode};
26use num_traits::FromPrimitive;
27use rust_decimal::{Decimal, MathematicalOps};
28use serde::{Deserialize, Serialize};
29
30use crate::registry::post_processor::PostProcessError;
31
32/// The base value for the tick system (1.0001).
33/// Each tick represents a 0.01% change in value.
34const TICK: f64 = 1.0001;
35
36/// The tick value that corresponds to a decimal value of 1.0.
37/// This serves as the midpoint of the tick scale.
38const MID_TICK: f64 = 262144.0;
39
40/// The maximum allowed tick value in the system.
41const MAX_TICK: f64 = 524287.0;
42
43/// The minimum allowed tick value in the system.
44const MIN_TICK: f64 = 1.0;
45
46/// Post-processor that converts decimal values to tick values.
47///
48/// The `TickPostProcessor` converts decimal values to tick values based on a logarithmic
49/// scale. This conversion can be useful for representing a wide range of values in a
50/// compact form with consistent relative precision.
51///
52/// # Tick Formula
53///
54/// The conversion uses the formula:
55/// ```text
56/// tick = log(value) / log(1.0001) + 262144
57/// ```
58///
59/// # Constraints
60///
61/// The resulting tick value must be within the range [1, 524287]. If the conversion
62/// yields a value outside this range, an error is returned.
63///
64/// # Examples
65///
66/// ```
67/// use bothan_lib::registry::post_processor::{PostProcessor, tick::TickPostProcessor};
68/// use rust_decimal::Decimal;
69///
70/// // Create a tick convertor post-processor
71/// let post_processor = PostProcessor::TickConvertor(TickPostProcessor {});
72///
73/// // Convert a decimal value to a tick value
74/// let value = Decimal::new(20, 0);  // The value 20.0
75/// let result = post_processor.post_process(value).unwrap();
76///
77/// // The result will be approximately 292102.8
78/// ```
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
80pub struct TickPostProcessor {}
81
82impl TickPostProcessor {
83    /// Converts a decimal value to its corresponding tick value.
84    ///
85    /// This method applies the logarithmic conversion to transform a decimal value
86    /// into a tick value. The tick value represents the position on a non-linear scale
87    /// where each step corresponds to a 0.01% change in value.
88    ///
89    /// # Errors
90    ///
91    /// Returns a `PostProcessError` if the computed tick value is outside the range [1, 524287].
92    pub fn process(&self, data: Decimal) -> Result<Decimal, PostProcessError> {
93        // Unwrap here is safe because the constants are hardcoded.
94        let tick = Decimal::from_f64(TICK).unwrap();
95        let min_tick = Decimal::from_f64(MIN_TICK).unwrap();
96        let mid_tick = Decimal::from_f64(MID_TICK).unwrap();
97        let max_tick = Decimal::from_f64(MAX_TICK).unwrap();
98
99        let result = (data.log10() / tick.log10()) + mid_tick;
100        if !(min_tick..=max_tick).contains(&result) {
101            return Err(PostProcessError::new("Tick value out of bound"));
102        }
103
104        Ok(result)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::registry::post_processor::PostProcessor;
112    use crate::registry::post_processor::tick::TickPostProcessor;
113
114    #[test]
115    fn test_process() {
116        let tick_convertor = PostProcessor::TickConvertor(TickPostProcessor {});
117        let result = tick_convertor.post_process(Decimal::from(20));
118        assert_eq!(
119            result.unwrap(),
120            Decimal::from_str_exact("292102.82057671349939971087257").unwrap()
121        );
122    }
123}