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}