Skip to main content

nexus_decimal/
financial.rs

1//! Financial-domain methods for `Decimal`.
2//!
3//! Trading operations that would otherwise be error-prone to implement
4//! manually: midpoint, spread, tick rounding, basis points, percentage
5//! calculations, and fused multiply-divide.
6
7use crate::Decimal;
8
9macro_rules! impl_decimal_financial {
10    ($backing:ty) => {
11        impl<const D: u8> Decimal<$backing, D> {
12            // ========================================================
13            // Price operations
14            // ========================================================
15
16            /// Midpoint of two prices: `(self + other) / 2`.
17            ///
18            /// Overflow-safe midpoint: `(self + other) / 2`.
19            ///
20            /// Uses the bit-manipulation formula `(a & b) + ((a ^ b) >> 1)`
21            /// which is correct for all representable values without
22            /// intermediate overflow.
23            ///
24            /// # Examples
25            ///
26            /// ```
27            /// use nexus_decimal::Decimal;
28            /// type D64 = Decimal<i64, 8>;
29            ///
30            /// let bid = D64::new(100, 0);
31            /// let ask = D64::new(101, 0);
32            /// assert_eq!(bid.midpoint(ask), D64::new(100, 50_000_000));
33            /// ```
34            #[inline(always)]
35            pub const fn midpoint(self, other: Self) -> Self {
36                // Overflow-safe integer average:
37                // avg(a, b) = (a & b) + ((a ^ b) >> 1)
38                // Correct for all values of the backing type, no overflow possible.
39                let a = self.value;
40                let b = other.value;
41                Self {
42                    value: (a & b) + ((a ^ b) >> 1),
43                }
44            }
45
46            /// Spread between two prices: `self - other`.
47            ///
48            /// Returns `None` if `self < other` (crossed market).
49            #[inline(always)]
50            pub const fn spread(self, other: Self) -> Option<Self> {
51                if self.value < other.value {
52                    None
53                } else {
54                    match self.value.checked_sub(other.value) {
55                        Some(v) => Some(Self { value: v }),
56                        None => None,
57                    }
58                }
59            }
60
61            /// Round to nearest tick size.
62            ///
63            /// `tick` must be positive. Rounds to the nearest multiple
64            /// of `tick` using banker's rounding on the remainder.
65            ///
66            /// # Examples
67            ///
68            /// ```
69            /// use nexus_decimal::Decimal;
70            /// type D64 = Decimal<i64, 8>;
71            ///
72            /// let price = D64::new(1, 23_700_000); // 1.237
73            /// let tick = D64::new(0, 5_000_000);   // 0.05
74            /// assert_eq!(price.round_to_tick(tick), Some(D64::new(1, 25_000_000))); // 1.25
75            /// ```
76            #[inline(always)]
77            pub const fn round_to_tick(self, tick: Self) -> Option<Self> {
78                assert!(tick.value > 0, "tick must be positive");
79                let remainder = self.value % tick.value;
80                if remainder == 0 {
81                    return Some(self);
82                }
83                let half_tick = tick.value / 2;
84                let base = self.value - remainder;
85
86                if remainder > half_tick {
87                    match base.checked_add(tick.value) {
88                        Some(v) => Some(Self { value: v }),
89                        None => None,
90                    }
91                } else if remainder < -half_tick {
92                    match base.checked_sub(tick.value) {
93                        Some(v) => Some(Self { value: v }),
94                        None => None,
95                    }
96                } else if remainder == half_tick || remainder == -half_tick {
97                    let quotient = self.value / tick.value;
98                    if quotient % 2 != 0 {
99                        if remainder > 0 {
100                            match base.checked_add(tick.value) {
101                                Some(v) => Some(Self { value: v }),
102                                None => None,
103                            }
104                        } else {
105                            match base.checked_sub(tick.value) {
106                                Some(v) => Some(Self { value: v }),
107                                None => None,
108                            }
109                        }
110                    } else {
111                        Some(Self { value: base })
112                    }
113                } else {
114                    Some(Self { value: base })
115                }
116            }
117
118            /// Floor to tick: round down to nearest multiple of `tick`.
119            ///
120            /// Returns `None` if the result would overflow.
121            #[inline(always)]
122            pub const fn floor_to_tick(self, tick: Self) -> Option<Self> {
123                assert!(tick.value > 0, "tick must be positive");
124                let remainder = self.value % tick.value;
125                if remainder >= 0 {
126                    Some(Self {
127                        value: self.value - remainder,
128                    })
129                } else {
130                    match (self.value - remainder).checked_sub(tick.value) {
131                        Some(v) => Some(Self { value: v }),
132                        None => None,
133                    }
134                }
135            }
136
137            /// Ceil to tick: round up to nearest multiple of `tick`.
138            ///
139            /// Returns `None` if the result would overflow.
140            #[inline(always)]
141            pub const fn ceil_to_tick(self, tick: Self) -> Option<Self> {
142                assert!(tick.value > 0, "tick must be positive");
143                let remainder = self.value % tick.value;
144                if remainder > 0 {
145                    match (self.value - remainder).checked_add(tick.value) {
146                        Some(v) => Some(Self { value: v }),
147                        None => None,
148                    }
149                } else if remainder < 0 {
150                    Some(Self {
151                        value: self.value - remainder,
152                    })
153                } else {
154                    Some(self)
155                }
156            }
157
158            // ========================================================
159            // Division shortcuts
160            // ========================================================
161
162            /// Divide by 2 using integer division. Truncates toward zero.
163            ///
164            /// The compiler optimizes this to a shift + sign-bit adjustment.
165            #[inline(always)]
166            pub const fn halve(self) -> Self {
167                Self {
168                    value: self.value / 2,
169                }
170            }
171
172            /// Divide by 10 using integer division.
173            #[inline(always)]
174            pub const fn div10(self) -> Self {
175                Self {
176                    value: self.value / 10,
177                }
178            }
179
180            /// Divide by 100 using integer division.
181            #[inline(always)]
182            pub const fn div100(self) -> Self {
183                Self {
184                    value: self.value / 100,
185                }
186            }
187
188            // ========================================================
189            // Comparison helpers
190            // ========================================================
191
192            /// Returns `true` if `self` is within `tolerance` of `other`.
193            ///
194            /// Equivalent to `|self - other| <= tolerance`. Returns `false`
195            /// when the difference overflows (values with opposite signs
196            /// near `MAX`/`MIN`).
197            #[inline]
198            pub const fn approx_eq(self, other: Self, tolerance: Self) -> bool {
199                let (diff, overflow) = if self.value >= other.value {
200                    self.value.overflowing_sub(other.value)
201                } else {
202                    other.value.overflowing_sub(self.value)
203                };
204                !overflow && diff <= tolerance.value
205            }
206
207            /// Clamp to a price range `[min, max]`.
208            #[inline]
209            pub const fn clamp_price(self, min: Self, max: Self) -> Self {
210                if self.value < min.value {
211                    min
212                } else if self.value > max.value {
213                    max
214                } else {
215                    self
216                }
217            }
218
219            // ========================================================
220            // Tick alignment and bps rounding
221            // ========================================================
222
223            /// Returns `true` if `self` is aligned to the given tick size.
224            ///
225            /// Panics if `tick` is not positive.
226            #[inline(always)]
227            pub const fn is_tick_aligned(self, tick: Self) -> bool {
228                assert!(tick.value > 0, "tick must be positive");
229                self.value % tick.value == 0
230            }
231
232            /// Round to nearest N basis points.
233            ///
234            /// Returns `None` if `n == 0` or the tick computation overflows.
235            ///
236            /// # Compile-time constraint
237            ///
238            /// Requires `D >= 4`. Referencing this method on a `Decimal`
239            /// with `D < 4` is a compile error.
240            #[inline(always)]
241            pub const fn round_bps(self, n: u32) -> Option<Self> {
242                const { assert!(D >= 4, "round_bps requires D >= 4") };
243                if n == 0 {
244                    return None;
245                }
246                let bp_raw = Self::SCALE / 10000;
247                let Some(tick_wide) = (bp_raw as i128).checked_mul(n as i128) else {
248                    return None;
249                };
250                if tick_wide > <$backing>::MAX as i128 || tick_wide <= 0 {
251                    return None;
252                }
253                self.round_to_tick(Self {
254                    value: tick_wide as $backing,
255                })
256            }
257
258            /// Floor to N basis points.
259            ///
260            /// Returns `None` if `n == 0` or the tick computation overflows.
261            ///
262            /// # Compile-time constraint
263            ///
264            /// Requires `D >= 4`. Referencing this method on a `Decimal`
265            /// with `D < 4` is a compile error.
266            #[inline(always)]
267            pub const fn floor_bps(self, n: u32) -> Option<Self> {
268                const { assert!(D >= 4, "floor_bps requires D >= 4") };
269                if n == 0 {
270                    return None;
271                }
272                let bp_raw = Self::SCALE / 10000;
273                let Some(tick_wide) = (bp_raw as i128).checked_mul(n as i128) else {
274                    return None;
275                };
276                if tick_wide > <$backing>::MAX as i128 || tick_wide <= 0 {
277                    return None;
278                }
279                self.floor_to_tick(Self {
280                    value: tick_wide as $backing,
281                })
282            }
283
284            /// Ceil to N basis points.
285            ///
286            /// Returns `None` if `n == 0` or the tick computation overflows.
287            ///
288            /// # Compile-time constraint
289            ///
290            /// Requires `D >= 4`. Referencing this method on a `Decimal`
291            /// with `D < 4` is a compile error.
292            #[inline(always)]
293            pub const fn ceil_bps(self, n: u32) -> Option<Self> {
294                const { assert!(D >= 4, "ceil_bps requires D >= 4") };
295                if n == 0 {
296                    return None;
297                }
298                let bp_raw = Self::SCALE / 10000;
299                let Some(tick_wide) = (bp_raw as i128).checked_mul(n as i128) else {
300                    return None;
301                };
302                if tick_wide > <$backing>::MAX as i128 || tick_wide <= 0 {
303                    return None;
304                }
305                self.ceil_to_tick(Self {
306                    value: tick_wide as $backing,
307                })
308            }
309        }
310    };
311}
312
313impl_decimal_financial!(i32);
314impl_decimal_financial!(i64);
315impl_decimal_financial!(i128);
316
317// ============================================================================
318// Methods that need widening (per-backing-type, not in shared macro)
319// ============================================================================
320
321// --- i32: widen to i64 for percent/bps calculations ---
322
323impl<const D: u8> Decimal<i32, D> {
324    /// Compute `self * percent / 100` via single truncating division.
325    ///
326    /// `percent` is in percentage points: 50 means 50%.
327    #[inline]
328    pub const fn percent_of(self, percent: Self) -> Option<Self> {
329        let product = (self.value as i64) * (percent.value as i64);
330        let scale_100 = (Self::SCALE as i64) * 100;
331        let result = product / scale_100;
332        if result > i32::MAX as i64 || result < i32::MIN as i64 {
333            None
334        } else {
335            Some(Self {
336                value: result as i32,
337            })
338        }
339    }
340
341    /// Convert to basis points: `self * 10000`.
342    #[inline]
343    pub const fn to_bps(self) -> Option<Self> {
344        self.mul_int(10_000)
345    }
346
347    /// Create from basis points: `bps / 10000`.
348    #[inline]
349    pub const fn from_bps(bps: i32) -> Option<Self> {
350        let scaled = bps as i64 * Self::SCALE as i64 / 10_000;
351        if scaled > i32::MAX as i64 || scaled < i32::MIN as i64 {
352            None
353        } else {
354            Some(Self {
355                value: scaled as i32,
356            })
357        }
358    }
359
360    /// Fused multiply-divide: `(self * a) / b` with single rounding.
361    #[inline]
362    pub const fn mul_div(self, mul: Self, div: Self) -> Option<Self> {
363        if div.value == 0 {
364            return None;
365        }
366        let product = (self.value as i64) * (mul.value as i64);
367        let result = product / (div.value as i64);
368        if result > i32::MAX as i64 || result < i32::MIN as i64 {
369            None
370        } else {
371            Some(Self {
372                value: result as i32,
373            })
374        }
375    }
376}
377
378// --- i64: widen to i128 for percent/bps calculations ---
379
380impl<const D: u8> Decimal<i64, D> {
381    /// Compute `self * percent / 100` via single truncating division.
382    ///
383    /// `percent` is in percentage points: 50 means 50%.
384    #[inline]
385    pub const fn percent_of(self, percent: Self) -> Option<Self> {
386        let product = (self.value as i128) * (percent.value as i128);
387        let scale_100 = (Self::SCALE as i128) * 100;
388        let result = product / scale_100;
389        if result > i64::MAX as i128 || result < i64::MIN as i128 {
390            None
391        } else {
392            Some(Self {
393                value: result as i64,
394            })
395        }
396    }
397
398    /// Convert to basis points: `self * 10000`.
399    #[inline]
400    pub const fn to_bps(self) -> Option<Self> {
401        self.mul_int(10_000)
402    }
403
404    /// Create from basis points: `bps / 10000`.
405    #[inline]
406    pub const fn from_bps(bps: i64) -> Option<Self> {
407        let scaled = (bps as i128) * (Self::SCALE as i128);
408        let value = scaled / 10_000;
409        if value > i64::MAX as i128 || value < i64::MIN as i128 {
410            None
411        } else {
412            Some(Self {
413                value: value as i64,
414            })
415        }
416    }
417
418    /// Fused multiply-divide: `(self * a) / b` with single rounding.
419    ///
420    /// Keeps the full i128 intermediate — single rounding at the end.
421    /// The primitive behind fee calculation, VWAP, cross-rates.
422    #[inline]
423    pub const fn mul_div(self, mul: Self, div: Self) -> Option<Self> {
424        if div.value == 0 {
425            return None;
426        }
427        let product = (self.value as i128) * (mul.value as i128);
428        let result = product / (div.value as i128);
429        if result > i64::MAX as i128 || result < i64::MIN as i128 {
430            None
431        } else {
432            Some(Self {
433                value: result as i64,
434            })
435        }
436    }
437}
438
439// ============================================================================
440// Bps/pct/tick operations (per-backing, widening)
441// ============================================================================
442
443macro_rules! impl_financial_widening {
444    ($backing:ty, $wider:ty) => {
445        impl<const D: u8> Decimal<$backing, D> {
446            /// N basis points of self: `self * bps / 10000`.
447            #[inline]
448            pub const fn bps_of(self, bps: i32) -> Option<Self> {
449                let product = (self.value as $wider) * (bps as $wider);
450                let result = product / 10000;
451                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
452                    None
453                } else {
454                    Some(Self {
455                        value: result as $backing,
456                    })
457                }
458            }
459
460            /// N percent of self: `self * pct / 100`.
461            #[inline]
462            pub const fn pct_of(self, pct: i32) -> Option<Self> {
463                let product = (self.value as $wider) * (pct as $wider);
464                let result = product / 100;
465                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
466                    None
467                } else {
468                    Some(Self {
469                        value: result as $backing,
470                    })
471                }
472            }
473
474            /// Adjust self by N basis points: `self * (10000 + bps) / 10000`.
475            #[inline]
476            pub const fn shift_bps(self, bps: i32) -> Option<Self> {
477                let factor = 10000_i64 + bps as i64;
478                let product = (self.value as $wider) * (factor as $wider);
479                let result = product / 10000;
480                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
481                    None
482                } else {
483                    Some(Self {
484                        value: result as $backing,
485                    })
486                }
487            }
488
489            /// Adjust self by N percent: `self * (100 + pct) / 100`.
490            #[inline]
491            pub const fn shift_pct(self, pct: i32) -> Option<Self> {
492                let factor = 100_i64 + pct as i64;
493                let product = (self.value as $wider) * (factor as $wider);
494                let result = product / 100;
495                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
496                    None
497                } else {
498                    Some(Self {
499                        value: result as $backing,
500                    })
501                }
502            }
503
504            /// Difference in bps relative to divisor:
505            /// `(self - other) / divisor * 10000`, returned as a Decimal.
506            ///
507            /// Uses multiply-before-divide: `diff * SCALE / divisor * 10000`
508            /// to preserve precision through the fixed-point division.
509            #[inline]
510            pub const fn bps_diff_by(self, other: Self, divisor: Self) -> Option<Self> {
511                if divisor.value == 0 {
512                    return None;
513                }
514                let diff = (self.value as $wider) - (other.value as $wider);
515                let diff_scaled = diff * (Self::SCALE as $wider);
516                let divisor_w = divisor.value as $wider;
517                let q = diff_scaled / divisor_w;
518                let r = diff_scaled % divisor_w;
519                let Some(main) = q.checked_mul(10000) else {
520                    return None;
521                };
522                let frac = r * 10000 / divisor_w;
523                let Some(result) = main.checked_add(frac) else {
524                    return None;
525                };
526                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
527                    None
528                } else {
529                    Some(Self {
530                        value: result as $backing,
531                    })
532                }
533            }
534
535            /// Difference in bps: `(self - other) * 10000 / other`.
536            #[inline]
537            pub const fn bps_diff(self, other: Self) -> Option<Self> {
538                self.bps_diff_by(other, other)
539            }
540
541            /// Percentage difference relative to divisor:
542            /// `(self - other) / divisor * 100`, returned as a Decimal.
543            #[inline]
544            pub const fn pct_diff_by(self, other: Self, divisor: Self) -> Option<Self> {
545                if divisor.value == 0 {
546                    return None;
547                }
548                let diff = (self.value as $wider) - (other.value as $wider);
549                let diff_scaled = diff * (Self::SCALE as $wider);
550                let divisor_w = divisor.value as $wider;
551                let q = diff_scaled / divisor_w;
552                let r = diff_scaled % divisor_w;
553                let Some(main) = q.checked_mul(100) else {
554                    return None;
555                };
556                let frac = r * 100 / divisor_w;
557                let Some(result) = main.checked_add(frac) else {
558                    return None;
559                };
560                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
561                    None
562                } else {
563                    Some(Self {
564                        value: result as $backing,
565                    })
566                }
567            }
568
569            /// Percentage difference: `(self - other) * 100 / other`.
570            #[inline]
571            pub const fn pct_diff(self, other: Self) -> Option<Self> {
572                self.pct_diff_by(other, other)
573            }
574
575            /// Returns `true` if `|self - other| <= |other| * bps / 10000`.
576            #[inline]
577            pub const fn within_bps(self, other: Self, bps: i32) -> bool {
578                if bps < 0 {
579                    return false;
580                }
581                let diff = if self.value >= other.value {
582                    (self.value as $wider) - (other.value as $wider)
583                } else {
584                    (other.value as $wider) - (self.value as $wider)
585                };
586                let other_abs = (other.value as $wider).abs();
587                let threshold = other_abs * (bps as $wider) / 10000;
588                diff <= threshold
589            }
590
591            /// Returns `true` if `|self - other| <= n * tick`.
592            ///
593            /// Panics if `tick` is not positive.
594            #[inline]
595            pub const fn within_ticks(self, other: Self, n: i64, tick: Self) -> bool {
596                assert!(tick.value > 0, "tick must be positive");
597                let diff = if self.value >= other.value {
598                    (self.value as $wider) - (other.value as $wider)
599                } else {
600                    (other.value as $wider) - (self.value as $wider)
601                };
602                let Some(threshold) = (n as $wider).checked_mul(tick.value as $wider) else {
603                    return n > 0;
604                };
605                diff <= threshold
606            }
607
608            /// `self + n * tick`. Returns `None` on overflow.
609            ///
610            /// Panics if `tick` is not positive.
611            #[inline]
612            pub const fn add_ticks(self, n: i64, tick: Self) -> Option<Self> {
613                assert!(tick.value > 0, "tick must be positive");
614                let Some(offset) = (n as $wider).checked_mul(tick.value as $wider) else {
615                    return None;
616                };
617                let Some(result) = (self.value as $wider).checked_add(offset) else {
618                    return None;
619                };
620                if result > <$backing>::MAX as $wider || result < <$backing>::MIN as $wider {
621                    None
622                } else {
623                    Some(Self {
624                        value: result as $backing,
625                    })
626                }
627            }
628
629            /// `(self - other) / tick` as an integer tick count.
630            ///
631            /// Truncates toward zero (partial ticks dropped, matching Rust
632            /// integer division).
633            ///
634            /// # Panics
635            ///
636            /// Panics if `tick` is not positive.
637            #[inline]
638            pub const fn tick_diff(self, other: Self, tick: Self) -> Option<i64> {
639                assert!(tick.value > 0, "tick must be positive");
640                let diff = (self.value as $wider) - (other.value as $wider);
641                let ticks = diff / (tick.value as $wider);
642                if ticks > i64::MAX as $wider || ticks < i64::MIN as $wider {
643                    None
644                } else {
645                    Some(ticks as i64)
646                }
647            }
648        }
649    };
650}
651
652impl_financial_widening!(i32, i64);
653impl_financial_widening!(i64, i128);
654
655// --- i128: uses wide arithmetic for percent/bps ---
656
657impl<const D: u8> Decimal<i128, D> {
658    /// Convert to basis points: `self * 10000`.
659    #[inline]
660    pub const fn to_bps(self) -> Option<Self> {
661        self.mul_int(10_000)
662    }
663
664    /// Create from basis points: `bps / 10000`.
665    #[inline]
666    pub const fn from_bps(bps: i128) -> Option<Self> {
667        match (bps).checked_mul(Self::SCALE) {
668            Some(scaled) => Some(Self {
669                value: scaled / 10_000,
670            }),
671            None => None,
672        }
673    }
674
675    /// Fused multiply-divide: `(self * a) / b` with single rounding.
676    ///
677    /// For i128, delegates to checked_mul then checked_div.
678    /// Not truly fused (two rounding events) — a 256-bit intermediate
679    /// would be needed for true single-rounding on i128.
680    #[inline]
681    pub fn mul_div(self, mul: Self, div: Self) -> Option<Self> {
682        if div.value == 0 {
683            return None;
684        }
685        let product = self.checked_mul(mul)?;
686        product.checked_div(div)
687    }
688
689    /// N basis points of self: `self * bps / 10000`.
690    ///
691    /// Uses decomposition to avoid intermediate overflow.
692    #[inline]
693    pub const fn bps_of(self, bps: i32) -> Option<Self> {
694        let q = self.value / 10000;
695        let r = self.value % 10000;
696        let Some(main) = q.checked_mul(bps as i128) else {
697            return None;
698        };
699        let frac = r * (bps as i128) / 10000;
700        match main.checked_add(frac) {
701            Some(v) => Some(Self { value: v }),
702            None => None,
703        }
704    }
705
706    /// N percent of self: `self * pct / 100`.
707    ///
708    /// Uses decomposition to avoid intermediate overflow.
709    #[inline]
710    pub const fn pct_of(self, pct: i32) -> Option<Self> {
711        let q = self.value / 100;
712        let r = self.value % 100;
713        let Some(main) = q.checked_mul(pct as i128) else {
714            return None;
715        };
716        let frac = r * (pct as i128) / 100;
717        match main.checked_add(frac) {
718            Some(v) => Some(Self { value: v }),
719            None => None,
720        }
721    }
722
723    /// Adjust self by N basis points: `self * (10000 + bps) / 10000`.
724    #[inline]
725    pub const fn shift_bps(self, bps: i32) -> Option<Self> {
726        let factor = 10000_i64 + bps as i64;
727        let q = self.value / 10000;
728        let r = self.value % 10000;
729        let Some(main) = q.checked_mul(factor as i128) else {
730            return None;
731        };
732        let frac = r * (factor as i128) / 10000;
733        match main.checked_add(frac) {
734            Some(v) => Some(Self { value: v }),
735            None => None,
736        }
737    }
738
739    /// Adjust self by N percent: `self * (100 + pct) / 100`.
740    #[inline]
741    pub const fn shift_pct(self, pct: i32) -> Option<Self> {
742        let factor = 100_i64 + pct as i64;
743        let q = self.value / 100;
744        let r = self.value % 100;
745        let Some(main) = q.checked_mul(factor as i128) else {
746            return None;
747        };
748        let frac = r * (factor as i128) / 100;
749        match main.checked_add(frac) {
750            Some(v) => Some(Self { value: v }),
751            None => None,
752        }
753    }
754
755    /// Difference in bps relative to divisor:
756    /// `(self - other) / divisor * 10000`, returned as a Decimal.
757    ///
758    /// Decomposes diff = q*divisor + r, then computes
759    /// `q * SCALE * 10000 + r * SCALE * 10000 / divisor`
760    /// to avoid intermediate overflow on i128.
761    #[inline]
762    pub const fn bps_diff_by(self, other: Self, divisor: Self) -> Option<Self> {
763        if divisor.value == 0 {
764            return None;
765        }
766        let Some(diff) = self.value.checked_sub(other.value) else {
767            return None;
768        };
769        let q = diff / divisor.value;
770        let r = diff % divisor.value;
771
772        let main = if q == 0 {
773            0
774        } else {
775            let Some(qs) = q.checked_mul(Self::SCALE) else {
776                return None;
777            };
778            let Some(qs10k) = qs.checked_mul(10000) else {
779                return None;
780            };
781            qs10k
782        };
783
784        let Some(rs) = r.checked_mul(Self::SCALE) else {
785            return None;
786        };
787        let rs_q = rs / divisor.value;
788        let rs_r = rs % divisor.value;
789        let Some(frac_main) = rs_q.checked_mul(10000) else {
790            return None;
791        };
792        // Sub-fractional term bounded by 9999 ULP; only overflows when
793        // |divisor| > i128::MAX / 10000 (~10^34 raw). Precision loss is
794        // negligible relative to the main result at that magnitude.
795        let frac_sub = match rs_r.checked_mul(10000) {
796            Some(v) => v / divisor.value,
797            None => 0,
798        };
799        let Some(frac) = frac_main.checked_add(frac_sub) else {
800            return None;
801        };
802
803        match main.checked_add(frac) {
804            Some(v) => Some(Self { value: v }),
805            None => None,
806        }
807    }
808
809    /// Difference in bps: `(self - other) * 10000 / other`.
810    #[inline]
811    pub const fn bps_diff(self, other: Self) -> Option<Self> {
812        self.bps_diff_by(other, other)
813    }
814
815    /// Percentage difference relative to divisor:
816    /// `(self - other) / divisor * 100`, returned as a Decimal.
817    #[inline]
818    pub const fn pct_diff_by(self, other: Self, divisor: Self) -> Option<Self> {
819        if divisor.value == 0 {
820            return None;
821        }
822        let Some(diff) = self.value.checked_sub(other.value) else {
823            return None;
824        };
825        let q = diff / divisor.value;
826        let r = diff % divisor.value;
827
828        let main = if q == 0 {
829            0
830        } else {
831            let Some(qs) = q.checked_mul(Self::SCALE) else {
832                return None;
833            };
834            let Some(qs100) = qs.checked_mul(100) else {
835                return None;
836            };
837            qs100
838        };
839
840        let Some(rs) = r.checked_mul(Self::SCALE) else {
841            return None;
842        };
843        let rs_q = rs / divisor.value;
844        let rs_r = rs % divisor.value;
845        let Some(frac_main) = rs_q.checked_mul(100) else {
846            return None;
847        };
848        // Sub-fractional term bounded by 99 ULP; same overflow condition
849        // as bps_diff_by — negligible precision loss at extreme magnitudes.
850        let frac_sub = match rs_r.checked_mul(100) {
851            Some(v) => v / divisor.value,
852            None => 0,
853        };
854        let Some(frac) = frac_main.checked_add(frac_sub) else {
855            return None;
856        };
857
858        match main.checked_add(frac) {
859            Some(v) => Some(Self { value: v }),
860            None => None,
861        }
862    }
863
864    /// Percentage difference: `(self - other) * 100 / other`.
865    #[inline]
866    pub const fn pct_diff(self, other: Self) -> Option<Self> {
867        self.pct_diff_by(other, other)
868    }
869
870    /// Returns `true` if `|self - other| <= |other| * bps / 10000`.
871    #[inline]
872    pub const fn within_bps(self, other: Self, bps: i32) -> bool {
873        if bps < 0 {
874            return false;
875        }
876        let abs_diff = if self.value >= other.value {
877            self.value.checked_sub(other.value)
878        } else {
879            other.value.checked_sub(self.value)
880        };
881        let Some(diff) = abs_diff else {
882            return false;
883        };
884        let other_abs = if other.value >= 0 {
885            Some(other.value)
886        } else {
887            other.value.checked_neg()
888        };
889        let Some(other_abs) = other_abs else {
890            return false;
891        };
892        match other_abs.checked_mul(bps as i128) {
893            Some(product) => diff <= product / 10000,
894            None => true,
895        }
896    }
897
898    /// Returns `true` if `|self - other| <= n * tick`.
899    ///
900    /// Panics if `tick` is not positive.
901    #[inline]
902    pub const fn within_ticks(self, other: Self, n: i64, tick: Self) -> bool {
903        assert!(tick.value > 0, "tick must be positive");
904        let abs_diff = if self.value >= other.value {
905            self.value.checked_sub(other.value)
906        } else {
907            other.value.checked_sub(self.value)
908        };
909        let Some(diff) = abs_diff else {
910            return false;
911        };
912        if n <= 0 {
913            return diff == 0 && n == 0;
914        }
915        match (n as i128).checked_mul(tick.value) {
916            Some(threshold) => diff <= threshold,
917            None => true,
918        }
919    }
920
921    /// `self + n * tick`. Returns `None` on overflow.
922    ///
923    /// Panics if `tick` is not positive.
924    #[inline]
925    pub const fn add_ticks(self, n: i64, tick: Self) -> Option<Self> {
926        assert!(tick.value > 0, "tick must be positive");
927        let Some(offset) = (n as i128).checked_mul(tick.value) else {
928            return None;
929        };
930        match self.value.checked_add(offset) {
931            Some(v) => Some(Self { value: v }),
932            None => None,
933        }
934    }
935
936    /// `(self - other) / tick` as an integer tick count.
937    ///
938    /// Truncates toward zero (partial ticks dropped, matching Rust
939    /// integer division).
940    ///
941    /// # Panics
942    ///
943    /// Panics if `tick` is not positive.
944    #[inline]
945    pub const fn tick_diff(self, other: Self, tick: Self) -> Option<i64> {
946        assert!(tick.value > 0, "tick must be positive");
947        let Some(diff) = self.value.checked_sub(other.value) else {
948            return None;
949        };
950        let ticks = diff / tick.value;
951        if ticks > i64::MAX as i128 || ticks < i64::MIN as i128 {
952            None
953        } else {
954            Some(ticks as i64)
955        }
956    }
957}