Skip to main content

flowsurface_data/chart/
value.rs

1use exchange::{
2    TickerInfo,
3    unit::{Power10, Price, PriceStep, Qty},
4};
5use serde::{Deserialize, Serialize};
6
7pub const Y_UNIT_STEP_FALLBACK: f32 = 1e-4;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
10pub struct YUnit(pub i64);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct YUnitRange {
14    min: YUnit,
15    max: YUnit,
16}
17
18impl YUnitRange {
19    pub fn new(min: YUnit, max: YUnit) -> Self {
20        if min <= max {
21            Self { min, max }
22        } else {
23            Self { min: max, max: min }
24        }
25    }
26
27    pub fn padded(self) -> Self {
28        let span = self.max.0.abs_diff(self.min.0);
29        let pad = if span == 0 {
30            self.max.0.unsigned_abs().max(1).saturating_add(49) / 50
31        } else {
32            span.saturating_mul(5).saturating_add(99) / 100
33        }
34        .max(1);
35        let pad = i64::try_from(pad).unwrap_or(i64::MAX);
36        Self::new(
37            YUnit(self.min.0.saturating_sub(pad)),
38            YUnit(self.max.0.saturating_add(pad)),
39        )
40    }
41
42    pub fn unit_at_ratio(self, ratio: f32) -> YUnit {
43        let ratio = ratio.clamp(0.0, 1.0);
44        let span = self.max.0.saturating_sub(self.min.0);
45        let offset = (span as f32 * ratio).round() as i64;
46        YUnit(self.min.0.saturating_add(offset).min(self.max.0))
47    }
48
49    pub fn min(self) -> YUnit {
50        self.min
51    }
52
53    pub fn max(self) -> YUnit {
54        self.max
55    }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum PanelValueLabelMode {
60    Compact,
61    Commas,
62    Abbreviated,
63}
64
65impl PanelValueLabelMode {
66    pub fn format_value(
67        self,
68        scale: PanelValueScale,
69        value: f64,
70        fallback_step: f64,
71        max_decimals: Option<u8>,
72    ) -> String {
73        match self {
74            Self::Compact => scale.format_compact(value, fallback_step, max_decimals),
75            Self::Commas => crate::util::format_with_commas(scale.quantize_value(value)),
76            Self::Abbreviated => crate::util::abbr_large_numbers(scale.quantize_value(value)),
77        }
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub struct PanelValueLabelPolicy {
83    pub axis_mode: PanelValueLabelMode,
84    pub header_mode: PanelValueLabelMode,
85    pub max_decimals: Option<u8>,
86}
87
88impl Default for PanelValueLabelPolicy {
89    fn default() -> Self {
90        Self {
91            axis_mode: PanelValueLabelMode::Compact,
92            header_mode: PanelValueLabelMode::Commas,
93            max_decimals: None,
94        }
95    }
96}
97
98impl PanelValueLabelPolicy {
99    pub fn format_axis_value(
100        self,
101        scale: PanelValueScale,
102        value: f64,
103        fallback_step: f64,
104    ) -> String {
105        self.axis_mode
106            .format_value(scale, value, fallback_step, self.max_decimals)
107    }
108
109    pub fn format_header_value(
110        self,
111        scale: PanelValueScale,
112        value: f64,
113        fallback_step: f64,
114    ) -> String {
115        self.header_mode
116            .format_value(scale, value, fallback_step, self.max_decimals)
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
121pub enum PanelValuePrecision {
122    BaseTickerMinTick,
123    BaseTickerMinQty,
124    FixedPower10(Power10<-8, 8>),
125    FixedStep(f64),
126}
127
128impl PanelValuePrecision {
129    const MIN_STEP_F64: f64 = 1e-8;
130
131    pub fn quantization_step_f32(self, base_ticker: Option<TickerInfo>) -> Option<f32> {
132        self.quantization_step(base_ticker).map(|step| step as f32)
133    }
134
135    pub fn quantization_step(self, base_ticker: Option<TickerInfo>) -> Option<f64> {
136        let step = match self {
137            Self::BaseTickerMinTick => base_ticker.map(|ticker| ticker.min_ticksize.as_f64()),
138            Self::BaseTickerMinQty => base_ticker.map(|ticker| ticker.min_qty.as_f64()),
139            Self::FixedPower10(step) => Some(step.as_f64()),
140            Self::FixedStep(step) => Some(step),
141        }?;
142
143        Some(step.abs().max(Self::MIN_STEP_F64))
144    }
145
146    pub fn value_bounds(self, base_ticker: Option<TickerInfo>) -> Option<(f64, f64)> {
147        let bounds = match self {
148            Self::BaseTickerMinTick => {
149                let unit_size =
150                    base_ticker.map(|ticker| Price::min_tick_units(ticker.min_ticksize))?;
151                let min_units = i64::MIN.div_euclid(unit_size).saturating_mul(unit_size);
152                let max_units = i64::MAX.div_euclid(unit_size).saturating_mul(unit_size);
153                (
154                    Price::from_units(min_units).to_f64(),
155                    Price::from_units(max_units).to_f64(),
156                )
157            }
158            Self::BaseTickerMinQty => {
159                let unit_size = base_ticker.map(|ticker| Qty::min_qty_units(ticker.min_qty))?;
160                let min_units = i64::MIN.div_euclid(unit_size).saturating_mul(unit_size);
161                let max_units = i64::MAX.div_euclid(unit_size).saturating_mul(unit_size);
162                (
163                    Qty::from_units(min_units).to_f64(),
164                    Qty::from_units(max_units).to_f64(),
165                )
166            }
167            Self::FixedPower10(step) => {
168                let step = step.as_f64().abs().max(Self::MIN_STEP_F64);
169                (i64::MIN as f64 * step, i64::MAX as f64 * step)
170            }
171            Self::FixedStep(step) => {
172                let step = step.abs().max(Self::MIN_STEP_F64);
173                (i64::MIN as f64 * step, i64::MAX as f64 * step)
174            }
175        };
176
177        (bounds.0.is_finite() && bounds.1.is_finite() && bounds.0 < bounds.1).then_some(bounds)
178    }
179
180    fn decimals_from_power10(power: i8) -> usize {
181        if power < 0 { (-power) as usize } else { 0 }
182    }
183
184    pub fn decimals_for_step(step: f64) -> usize {
185        let step = step.abs();
186        if !step.is_finite() || step <= 0.0 {
187            return 4;
188        }
189
190        for decimals in 0..=8 {
191            let scaled = step * 10_f64.powi(decimals as i32);
192            let nearest = scaled.round();
193            let tolerance = (scaled.abs() * 1e-9).max(1e-12);
194
195            if (scaled - nearest).abs() <= tolerance {
196                return decimals;
197            }
198        }
199
200        8
201    }
202
203    pub fn decimals(self, base_ticker: Option<TickerInfo>) -> Option<usize> {
204        match self {
205            Self::BaseTickerMinTick => {
206                base_ticker.map(|ticker| Self::decimals_from_power10(ticker.min_ticksize.power))
207            }
208            Self::BaseTickerMinQty => {
209                base_ticker.map(|ticker| Self::decimals_from_power10(ticker.min_qty.power))
210            }
211            Self::FixedPower10(step) => Some(Self::decimals_from_power10(step.power)),
212            Self::FixedStep(step) => Some(Self::decimals_for_step(step)),
213        }
214    }
215
216    pub fn value_to_unit_f32(
217        self,
218        value: f32,
219        base_ticker: Option<TickerInfo>,
220        fallback_step: f32,
221    ) -> YUnit {
222        self.value_to_unit(f64::from(value), base_ticker, f64::from(fallback_step))
223    }
224
225    /// Convert an exact price to display units when the precision is price-native.
226    /// `None` means the precision uses another numeric domain or an arbitrary step.
227    pub fn price_to_unit(self, value: Price, base_ticker: Option<TickerInfo>) -> Option<YUnit> {
228        match self {
229            Self::BaseTickerMinTick => {
230                let ticker = base_ticker?;
231                let min_tick = ticker.min_ticksize;
232                let unit_size = Price::min_tick_units(min_tick);
233                Some(YUnit(
234                    value
235                        .round_to_min_tick(min_tick)
236                        .units
237                        .div_euclid(unit_size),
238                ))
239            }
240            Self::FixedPower10(step) => {
241                let step_value = step.as_f64();
242                let step_price = Price::from_f64(step_value);
243                if step_price.units <= 0 || step_price.to_f64() != step_value {
244                    return None;
245                }
246                Some(YUnit(
247                    value
248                        .round_to_step(PriceStep {
249                            units: step_price.units,
250                        })
251                        .units
252                        .div_euclid(step_price.units),
253                ))
254            }
255            Self::BaseTickerMinQty | Self::FixedStep(_) => None,
256        }
257    }
258
259    pub fn value_to_unit(
260        self,
261        value: f64,
262        base_ticker: Option<TickerInfo>,
263        fallback_step: f64,
264    ) -> YUnit {
265        if !value.is_finite() {
266            return YUnit(0);
267        }
268
269        match self {
270            Self::BaseTickerMinTick => {
271                if let Some(ticker) = base_ticker {
272                    let min_tick = ticker.min_ticksize;
273                    let unit_size = Price::min_tick_units(min_tick);
274                    let price_units = Price::from_f64(value).round_to_min_tick(min_tick).units;
275                    return YUnit(price_units.div_euclid(unit_size));
276                }
277            }
278            Self::BaseTickerMinQty => {
279                if let Some(ticker) = base_ticker {
280                    let min_qty = ticker.min_qty;
281                    let unit_size = Qty::min_qty_units(min_qty);
282                    let qty_units = Qty::from_f64(value).round_to_min_qty(min_qty).units;
283                    return YUnit(qty_units.div_euclid(unit_size));
284                }
285            }
286            Self::FixedPower10(_) | Self::FixedStep(_) => {}
287        }
288
289        let step = self
290            .quantization_step(base_ticker)
291            .unwrap_or(fallback_step.abs().max(Self::MIN_STEP_F64));
292        YUnit((value / step).round() as i64)
293    }
294
295    pub fn unit_to_value_f32(
296        self,
297        y_unit: YUnit,
298        base_ticker: Option<TickerInfo>,
299        fallback_step: f32,
300    ) -> f32 {
301        self.unit_to_value(y_unit, base_ticker, f64::from(fallback_step)) as f32
302    }
303
304    pub fn unit_to_value(
305        self,
306        y_unit: YUnit,
307        base_ticker: Option<TickerInfo>,
308        fallback_step: f64,
309    ) -> f64 {
310        match self {
311            Self::BaseTickerMinTick => {
312                if let Some(ticker) = base_ticker {
313                    let min_tick = ticker.min_ticksize;
314                    let unit_size = Price::min_tick_units(min_tick);
315                    let units = y_unit.0.saturating_mul(unit_size);
316                    return Price::from_units(units).to_f64();
317                }
318            }
319            Self::BaseTickerMinQty => {
320                if let Some(ticker) = base_ticker {
321                    let min_qty = ticker.min_qty;
322                    let unit_size = Qty::min_qty_units(min_qty);
323                    let units = y_unit.0.saturating_mul(unit_size);
324                    return Qty::from_units(units).to_f64();
325                }
326            }
327            Self::FixedPower10(_) | Self::FixedStep(_) => {}
328        }
329
330        let step = self
331            .quantization_step(base_ticker)
332            .unwrap_or(fallback_step.abs().max(Self::MIN_STEP_F64));
333        (y_unit.0 as f64) * step
334    }
335}
336
337#[derive(Debug, Clone, Copy)]
338pub struct PanelValueScale {
339    precision: Option<PanelValuePrecision>,
340    base_ticker: Option<TickerInfo>,
341}
342
343impl PanelValueScale {
344    fn fallback_value_to_unit_f32(value: f32) -> YUnit {
345        if !value.is_finite() {
346            return YUnit(0);
347        }
348
349        YUnit((value / Y_UNIT_STEP_FALLBACK).round() as i64)
350    }
351
352    fn fallback_value_to_unit(value: f64) -> YUnit {
353        if !value.is_finite() {
354            return YUnit(0);
355        }
356
357        YUnit((value / f64::from(Y_UNIT_STEP_FALLBACK)).round() as i64)
358    }
359
360    pub fn new(precision: Option<PanelValuePrecision>, base_ticker: Option<TickerInfo>) -> Self {
361        Self {
362            precision,
363            base_ticker,
364        }
365    }
366
367    pub fn minimum_span(self) -> f64 {
368        self.quantization_step()
369            .unwrap_or(f64::from(Y_UNIT_STEP_FALLBACK))
370            .abs()
371            .max(1e-8)
372            * 8.0
373    }
374
375    pub fn value_bounds(self) -> (f64, f64) {
376        self.precision
377            .and_then(|precision| precision.value_bounds(self.base_ticker))
378            .unwrap_or_else(|| {
379                let step = f64::from(Y_UNIT_STEP_FALLBACK);
380                (i64::MIN as f64 * step, i64::MAX as f64 * step)
381            })
382    }
383
384    pub fn quantize_value(self, value: f64) -> f64 {
385        self.unit_to_value(self.value_to_unit(value))
386    }
387
388    fn format_compact(self, value: f64, fallback_step: f64, max_decimals: Option<u8>) -> String {
389        let quantized = self.quantize_value(value);
390        let fallback = fallback_step.abs().max(1e-6);
391        let decimals = self
392            .precision
393            .and_then(|precision| precision.decimals(self.base_ticker))
394            .unwrap_or_else(|| PanelValuePrecision::decimals_for_step(fallback));
395        let decimals = max_decimals
396            .map(|max| decimals.min(max as usize))
397            .unwrap_or(decimals);
398        format!("{quantized:.decimals$}")
399    }
400
401    pub fn quantization_step_f32(self) -> Option<f32> {
402        self.precision
403            .and_then(|precision| precision.quantization_step_f32(self.base_ticker))
404    }
405
406    pub fn quantization_step(self) -> Option<f64> {
407        self.precision
408            .and_then(|precision| precision.quantization_step(self.base_ticker))
409    }
410
411    pub fn value_to_unit_f32(self, value: f32) -> YUnit {
412        self.precision
413            .map(|precision| {
414                precision.value_to_unit_f32(value, self.base_ticker, Y_UNIT_STEP_FALLBACK)
415            })
416            .unwrap_or_else(|| Self::fallback_value_to_unit_f32(value))
417    }
418
419    pub fn value_to_unit(self, value: f64) -> YUnit {
420        self.precision
421            .map(|precision| {
422                precision.value_to_unit(value, self.base_ticker, f64::from(Y_UNIT_STEP_FALLBACK))
423            })
424            .unwrap_or_else(|| Self::fallback_value_to_unit(value))
425    }
426
427    pub fn unit_to_value_f32(self, unit: YUnit) -> f32 {
428        self.precision
429            .map(|precision| {
430                precision.unit_to_value_f32(unit, self.base_ticker, Y_UNIT_STEP_FALLBACK)
431            })
432            .unwrap_or_else(|| (unit.0 as f32) * Y_UNIT_STEP_FALLBACK)
433    }
434
435    pub fn unit_to_value(self, unit: YUnit) -> f64 {
436        self.precision
437            .map(|precision| {
438                precision.unit_to_value(unit, self.base_ticker, f64::from(Y_UNIT_STEP_FALLBACK))
439            })
440            .unwrap_or_else(|| (unit.0 as f64) * f64::from(Y_UNIT_STEP_FALLBACK))
441    }
442
443    pub fn price_to_unit(self, value: Price) -> YUnit {
444        self.precision
445            .and_then(|precision| precision.price_to_unit(value, self.base_ticker))
446            .unwrap_or_else(|| self.value_to_unit_f32(value.to_f32_lossy()))
447    }
448
449    pub fn values(self, range: YUnitRange) -> (f64, f64) {
450        (
451            self.unit_to_value(range.min()),
452            self.unit_to_value(range.max()),
453        )
454    }
455
456    pub fn values_f32(self, range: YUnitRange) -> (f32, f32) {
457        (
458            self.unit_to_value_f32(range.min()),
459            self.unit_to_value_f32(range.max()),
460        )
461    }
462}