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 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}