architect_api/symbology/
execution_info.rs

1use super::ExecutionVenue;
2use crate::Dir;
3use derive_more::Display;
4use rust_decimal::Decimal;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use strum_macros::{EnumString, IntoStaticStr};
8
9/// Information about a symbol related to its execution route.
10#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
11pub struct ExecutionInfo {
12    pub execution_venue: ExecutionVenue,
13    /// If the execution venue has stable symbology, this may be populated
14    pub exchange_symbol: Option<String>,
15    pub tick_size: TickSize,
16    pub step_size: Decimal,
17    pub min_order_quantity: Decimal,
18    pub min_order_quantity_unit: MinOrderQuantityUnit,
19    pub is_delisted: bool,
20    pub initial_margin: Option<Decimal>,
21    pub maintenance_margin: Option<Decimal>,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
25#[serde(rename_all = "snake_case")]
26pub enum TickSize {
27    #[schemars(title = "Simple|Decimal")]
28    Simple(Decimal),
29    /// List of (threshold, tick_size) pairs.  For price greater than or equal
30    /// to each threshold, the tick size is the corresponding value.
31    ///
32    /// For example, if the thresholds are [(100, 0.01), (200, 0.02)], valid
33    /// prices include:
34    ///
35    /// ...199.98, 199.99, 200.00, 200.02, 200.04...
36    #[schemars(title = "Varying")]
37    Varying { thresholds: Vec<(Decimal, Decimal)> },
38}
39
40impl TickSize {
41    pub fn simple(tick_size: Decimal) -> Self {
42        Self::Simple(tick_size)
43    }
44
45    /// Increment the price by `n` ticks.  Negative values decrement.
46    ///
47    /// This method assumes:
48    ///
49    /// - `price` is a valid price (i.e. on a tick and not between ticks)
50    /// - `price` is not on or below the first threshold, for negative `n`
51    /// - `thresholds` is non-empty, if tick size is `Varying`
52    /// - `thresholds` are well-formed[1]
53    ///
54    /// [1] Sequential thresholds must be arithemetically adjacent; e.g.
55    /// `(100, 0.3), (200, _)` is an invalid threshold sequence because
56    /// no iteration of ticks can get from 100 to 200.
57    pub fn increment(&self, mut price: Decimal, mut n: i32) -> Option<Decimal> {
58        if n == 0 {
59            return Some(price);
60        } else if n < 0 {
61            return self.decrement(price, -n);
62        }
63        // INVARIANT: n > 0
64        let thresholds = match self {
65            TickSize::Simple(step) => {
66                return Some(price + *step * Decimal::from(n));
67            }
68            TickSize::Varying { thresholds } => thresholds,
69        };
70        if thresholds.is_empty() {
71            return None;
72        }
73        // INVARIANT: thresholds.len() > 0
74        let mut i = thresholds.len() - 1;
75        let mut t_bound;
76        let mut t_step;
77        let mut u_bound;
78        loop {
79            (t_bound, t_step) = thresholds[i];
80            u_bound = thresholds.get(i + 1).map(|(b, _)| *b);
81            if price >= t_bound {
82                break;
83            }
84            if i == 0 {
85                // didn't find a threshold
86                return None;
87            }
88            i -= 1;
89        }
90        while n > 0 {
91            price += t_step;
92            if u_bound.is_some_and(|u| price >= u) {
93                // INVARIANT: threshold[i + 1] exists
94                // move to next threshold
95                i += 1;
96                (_, t_step) = thresholds[i];
97                u_bound = thresholds.get(i + 1).map(|(b, _)| *b);
98            }
99            n -= 1;
100        }
101        Some(price)
102    }
103
104    pub fn decrement(&self, mut price: Decimal, mut n: i32) -> Option<Decimal> {
105        if n == 0 {
106            return Some(price);
107        } else if n < 0 {
108            return self.increment(price, -n);
109        }
110        // INVARIANT: n > 0
111        let thresholds = match self {
112            TickSize::Simple(step) => {
113                return Some(price - *step * Decimal::from(n));
114            }
115            TickSize::Varying { thresholds } => thresholds,
116        };
117        if thresholds.is_empty() {
118            return None;
119        }
120        // INVARIANT: thresholds.len() > 0
121        let mut i = thresholds.len() - 1;
122        let mut t_bound;
123        let mut t_step;
124        loop {
125            (t_bound, t_step) = thresholds[i];
126            if price >= t_bound {
127                break;
128            }
129            if i == 0 {
130                // didn't find a threshold
131                return None;
132            }
133            i -= 1;
134        }
135        while n > 0 {
136            if price == t_bound {
137                // on boundary, use previous threshold tick size
138                if i == 0 {
139                    return None;
140                }
141                i -= 1;
142                (t_bound, t_step) = thresholds[i];
143            }
144            price -= t_step;
145            n -= 1;
146        }
147        Some(price)
148    }
149
150    /// Round the price to make the price more aggressive for the given direction.
151    pub fn round_aggressive(&self, price: Decimal, dir: Dir) -> Option<Decimal> {
152        match dir {
153            Dir::Buy => self.round_up(price),
154            Dir::Sell => self.round_down(price),
155        }
156    }
157
158    /// Round the price to make the price more passive for the given direction.
159    pub fn round_passive(&self, price: Decimal, dir: Dir) -> Option<Decimal> {
160        match dir {
161            Dir::Buy => self.round_down(price),
162            Dir::Sell => self.round_up(price),
163        }
164    }
165
166    pub fn round_up(&self, price: Decimal) -> Option<Decimal> {
167        match self {
168            TickSize::Simple(tick_size) => {
169                if tick_size.is_zero() {
170                    return None;
171                }
172                let remainder = price % tick_size;
173                if remainder.is_zero() {
174                    // Already on a tick
175                    Some(price)
176                } else if remainder > Decimal::ZERO {
177                    // Positive remainder: round up to next tick
178                    Some(price - remainder + tick_size)
179                } else {
180                    // Negative remainder: already rounded up
181                    Some(price - remainder)
182                }
183            }
184            TickSize::Varying { thresholds } => {
185                if thresholds.is_empty() {
186                    return None;
187                }
188
189                // Find the appropriate tick size for this price
190                let mut tick_size = thresholds[0].1;
191                for (threshold, size) in thresholds {
192                    if price >= *threshold {
193                        tick_size = *size;
194                    } else {
195                        break;
196                    }
197                }
198
199                if tick_size.is_zero() {
200                    return None;
201                }
202
203                let remainder = price % tick_size;
204                if remainder.is_zero() {
205                    // Already on a tick
206                    Some(price)
207                } else if remainder > Decimal::ZERO {
208                    // Positive remainder: round up to next tick
209                    Some(price - remainder + tick_size)
210                } else {
211                    // Negative remainder: already rounded up
212                    Some(price - remainder)
213                }
214            }
215        }
216    }
217
218    pub fn round_down(&self, price: Decimal) -> Option<Decimal> {
219        match self {
220            TickSize::Simple(tick_size) => {
221                if tick_size.is_zero() {
222                    return None;
223                }
224                let remainder = price % tick_size;
225                if remainder.is_zero() {
226                    // Already on a tick
227                    Some(price)
228                } else if remainder > Decimal::ZERO {
229                    // Positive remainder: round down
230                    Some(price - remainder)
231                } else {
232                    // Negative remainder: need to go down one more tick
233                    Some(price - remainder - tick_size)
234                }
235            }
236            TickSize::Varying { thresholds } => {
237                if thresholds.is_empty() {
238                    return None;
239                }
240
241                // Find the appropriate tick size for this price
242                let mut tick_size = thresholds[0].1;
243                for (threshold, size) in thresholds {
244                    if price >= *threshold {
245                        tick_size = *size;
246                    } else {
247                        break;
248                    }
249                }
250
251                if tick_size.is_zero() {
252                    return None;
253                }
254
255                let remainder = price % tick_size;
256                if remainder.is_zero() {
257                    // Already on a tick
258                    Some(price)
259                } else if remainder > Decimal::ZERO {
260                    // Positive remainder: round down
261                    Some(price - remainder)
262                } else {
263                    // Negative remainder: need to go down one more tick
264                    Some(price - remainder - tick_size)
265                }
266            }
267        }
268    }
269
270    /// Number of ticks between `from` to `to` based on the tick size.
271    ///
272    /// If `from` is less than `to`, the result is positive.  If `from` is
273    /// greater than `to`, the result is negative.
274    ///
275    /// If step size is zero, return None.
276    pub fn signed_tick_distance(&self, from: Decimal, to: Decimal) -> Option<Decimal> {
277        if from == to {
278            return Some(Decimal::ZERO);
279        } else if from > to {
280            return self.signed_tick_distance(to, from).map(|d| -d);
281        }
282        // INVARIANT: from < to
283        match self {
284            TickSize::Simple(step) => {
285                if step.is_zero() {
286                    None
287                } else {
288                    Some((to - from) / *step)
289                }
290            }
291            TickSize::Varying { thresholds } => {
292                let mut ticks = Decimal::ZERO;
293                let mut price = from;
294                let mut i = thresholds.iter();
295                let mut step = None;
296                // find the first threshold including the price
297                while let Some((lower, lower_step)) = i.next() {
298                    if price >= *lower {
299                        step = Some(*lower_step);
300                        break;
301                    }
302                }
303                let mut step = step?; // threshold doesn't include price at all, impossible to compute
304                loop {
305                    if step.is_zero() {
306                        return None;
307                    }
308                    match i.next() {
309                        Some((upper, next_step)) => {
310                            // `to` is beyond the upper threshold, compute distance to next threshold
311                            if to >= *upper {
312                                ticks += (upper - price) / step;
313                                price = *upper;
314                                step = *next_step;
315                            } else {
316                                // `to` is below the upper threshold, compute distance to `to` and done
317                                ticks += (to - price) / step;
318                                break;
319                            }
320                        }
321                        None => {
322                            // no more thresholds, compute straightforward distance
323                            ticks += (to - price) / step;
324                            break;
325                        }
326                    }
327                }
328                Some(ticks)
329            }
330        }
331    }
332}
333
334impl ExecutionInfo {
335    /// Calculate the minimum quantity in base units
336    /// When min_order_quantity_unit is Quote, converts using the provided price
337    /// Returns Decimal::ZERO if price is needed but not provided or is zero
338    pub fn min_quantity_in_base_units(&self, price: Option<Decimal>) -> Option<Decimal> {
339        match self.min_order_quantity_unit {
340            MinOrderQuantityUnit::Base => Some(self.min_order_quantity),
341            MinOrderQuantityUnit::Quote => {
342                if let Some(p) = price {
343                    if p.is_zero() {
344                        None
345                    } else {
346                        Some(self.min_order_quantity / p)
347                    }
348                } else {
349                    // If price not provided for Quote unit, can't enforce minimum
350                    None
351                }
352            }
353        }
354    }
355
356    /// Round quantity to the nearest valid step size without checking minimum
357    pub fn round_quantity(&self, quantity: Decimal) -> Decimal {
358        round_quantity_with_step(quantity, self.step_size)
359    }
360
361    /// Round quantity up to the next valid step size without respecting minimum order quantity
362    pub fn round_quantity_up(&self, quantity: Decimal) -> Decimal {
363        round_quantity_up_with_step(quantity, self.step_size)
364    }
365
366    /// Round quantity down to the previous valid step size without respecting minimum order quantity
367    pub fn round_quantity_down(&self, quantity: Decimal) -> Decimal {
368        round_quantity_down_with_step(quantity, self.step_size)
369    }
370}
371
372/// Round quantity to the nearest valid step size
373pub fn round_quantity_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
374    if step_size.is_zero() {
375        return quantity;
376    }
377    let remainder = quantity % step_size;
378    if remainder.is_zero() {
379        quantity
380    } else {
381        let half_step = step_size / Decimal::from(2);
382        if remainder >= half_step {
383            // Round up
384            quantity - remainder + step_size
385        } else {
386            // Round down
387            quantity - remainder
388        }
389    }
390}
391
392/// Round quantity up to the next valid step size
393pub fn round_quantity_up_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
394    if step_size.is_zero() {
395        return quantity;
396    }
397
398    let remainder = quantity % step_size;
399    if remainder.is_zero() {
400        quantity
401    } else {
402        quantity - remainder + step_size
403    }
404}
405
406/// Round quantity down to the previous valid step size
407pub fn round_quantity_down_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
408    if step_size.is_zero() {
409        return quantity;
410    }
411
412    let remainder = quantity % step_size;
413    if remainder.is_zero() {
414        quantity
415    } else {
416        quantity - remainder
417    }
418}
419
420// TODO: un snake_case this
421#[derive(
422    Default,
423    Debug,
424    Display,
425    Clone,
426    Copy,
427    EnumString,
428    IntoStaticStr,
429    Serialize,
430    Deserialize,
431    JsonSchema,
432)]
433#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
434#[serde(tag = "unit", rename_all = "snake_case")]
435#[strum(serialize_all = "snake_case")]
436pub enum MinOrderQuantityUnit {
437    #[default]
438    #[schemars(title = "Base")]
439    Base,
440    #[schemars(title = "Quote")]
441    Quote,
442}
443
444#[cfg(feature = "postgres")]
445crate::to_sql_str!(MinOrderQuantityUnit);
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use rust_decimal_macros::dec;
451
452    #[test]
453    fn test_round_quantity() {
454        let step_size = dec!(0.1);
455
456        assert_eq!(round_quantity_with_step(dec!(1.24), step_size), dec!(1.2));
457        assert_eq!(round_quantity_with_step(dec!(1.25), step_size), dec!(1.3));
458        assert_eq!(round_quantity_with_step(dec!(0.04), step_size), dec!(0.0));
459
460        assert_eq!(round_quantity_up_with_step(dec!(1.21), step_size), dec!(1.3));
461        assert_eq!(round_quantity_up_with_step(dec!(0.01), step_size), dec!(0.1));
462        assert_eq!(round_quantity_up_with_step(dec!(0.0), step_size), dec!(0.0));
463
464        assert_eq!(round_quantity_down_with_step(dec!(1.29), step_size), dec!(1.2));
465        assert_eq!(round_quantity_down_with_step(dec!(0.09), step_size), dec!(0.0));
466        assert_eq!(round_quantity_down_with_step(dec!(0.0), step_size), dec!(0.0));
467    }
468
469    #[test]
470    fn test_tick_size_decrement() {
471        let tick = TickSize::Simple(dec!(0.01));
472        assert_eq!(tick.increment(dec!(100.00), -1), Some(dec!(99.99)));
473
474        let tick_varying = TickSize::Varying {
475            thresholds: vec![
476                (dec!(0), dec!(0.01)),
477                (dec!(100), dec!(0.05)),
478                (dec!(500), dec!(0.10)),
479            ],
480        };
481
482        // Test on boundary with negative increment
483        assert_eq!(tick_varying.increment(dec!(100.00), -1), Some(dec!(99.99)));
484        assert_eq!(tick_varying.increment(dec!(100.05), -1), Some(dec!(100)));
485        assert_eq!(tick_varying.increment(dec!(500.00), -1), Some(dec!(499.95)));
486    }
487
488    #[test]
489    fn test_tick_size_zero() {
490        let tick = TickSize::Varying {
491            thresholds: vec![
492                (dec!(0), dec!(0.01)),
493                (dec!(100), dec!(0.05)),
494                (dec!(500), dec!(0.10)),
495            ],
496        };
497
498        assert_eq!(tick.increment(dec!(100.00), 0), Some(dec!(100.00)));
499        assert_eq!(tick.increment(dec!(150.00), 0), Some(dec!(150.00)));
500        assert_eq!(tick.increment(dec!(50.00), 0), Some(dec!(50.00)));
501
502        // Note: With the assumption that price is NOT below first threshold,
503        // this test case is no longer valid
504    }
505
506    #[test]
507    fn test_tick_size_boundary_behavior() {
508        let tick = TickSize::Varying {
509            thresholds: vec![
510                (dec!(0), dec!(0.01)),
511                (dec!(100), dec!(0.05)),
512                (dec!(500), dec!(0.10)),
513            ],
514        };
515
516        // On boundary going up uses new tick size
517        assert_eq!(tick.increment(dec!(100.00), 1), Some(dec!(100.05)));
518        assert_eq!(tick.increment(dec!(500.00), 1), Some(dec!(500.10)));
519
520        // On boundary going down uses previous tick size
521        assert_eq!(tick.increment(dec!(100.00), -1), Some(dec!(99.99)));
522        assert_eq!(tick.increment(dec!(500.00), -1), Some(dec!(499.95)));
523    }
524
525    #[test]
526    fn test_tick_size_multiple_crossings() {
527        let tick = TickSize::Varying {
528            thresholds: vec![
529                (dec!(0), dec!(0.01)),
530                (dec!(100), dec!(0.05)),
531                (dec!(500), dec!(0.10)),
532            ],
533        };
534
535        // Cross multiple thresholds going up
536        let result = tick.increment(dec!(50), 12000);
537        assert_eq!(result, Some(dec!(450)));
538
539        // Test crossing multiple thresholds going down
540        // With the boundary handling:
541        // - 1000 ticks to get from 600 to 500
542        // - Then we're at 500, one more tick at 0.05 gets us to 499.95
543        // - Remaining ticks: 5100 - 1000 - 1 = 4099
544        // - 4099 * 0.05 = 204.95 movement
545        // - 499.95 - 204.95 = 295.00
546        let result = tick.increment(dec!(600), -5100);
547        assert_eq!(result, Some(dec!(295.00)));
548    }
549
550    #[test]
551    fn test_round_up_simple() {
552        let tick = TickSize::Simple(dec!(0.01));
553
554        // Already on tick
555        assert_eq!(tick.round_up(dec!(100.00)), Some(dec!(100.00)));
556
557        // Between ticks - should round up
558        assert_eq!(tick.round_up(dec!(100.001)), Some(dec!(100.01)));
559        assert_eq!(tick.round_up(dec!(100.005)), Some(dec!(100.01)));
560        assert_eq!(tick.round_up(dec!(100.009)), Some(dec!(100.01)));
561
562        // Negative prices
563        assert_eq!(tick.round_up(dec!(-99.995)), Some(dec!(-99.99)));
564        assert_eq!(tick.round_up(dec!(-100.00)), Some(dec!(-100.00)));
565
566        // Zero tick size
567        let zero_tick = TickSize::Simple(dec!(0));
568        assert_eq!(zero_tick.round_up(dec!(100.123)), None);
569    }
570
571    #[test]
572    fn test_round_down_simple() {
573        let tick = TickSize::Simple(dec!(0.01));
574
575        // Already on tick
576        assert_eq!(tick.round_down(dec!(100.00)), Some(dec!(100.00)));
577
578        // Between ticks - should round down
579        assert_eq!(tick.round_down(dec!(100.001)), Some(dec!(100.00)));
580        assert_eq!(tick.round_down(dec!(100.005)), Some(dec!(100.00)));
581        assert_eq!(tick.round_down(dec!(100.009)), Some(dec!(100.00)));
582
583        // Negative prices
584        assert_eq!(tick.round_down(dec!(-99.995)), Some(dec!(-100.00)));
585        assert_eq!(tick.round_down(dec!(-100.00)), Some(dec!(-100.00)));
586
587        // Zero tick size
588        let zero_tick = TickSize::Simple(dec!(0));
589        assert_eq!(zero_tick.round_down(dec!(100.123)), None);
590    }
591
592    #[test]
593    fn test_round_up_varying() {
594        let tick = TickSize::Varying {
595            thresholds: vec![
596                (dec!(0), dec!(0.01)),
597                (dec!(100), dec!(0.05)),
598                (dec!(500), dec!(0.10)),
599            ],
600        };
601
602        // In first threshold range
603        assert_eq!(tick.round_up(dec!(50.00)), Some(dec!(50.00)));
604        assert_eq!(tick.round_up(dec!(50.001)), Some(dec!(50.01)));
605        assert_eq!(tick.round_up(dec!(99.996)), Some(dec!(100.00)));
606
607        // In second threshold range
608        assert_eq!(tick.round_up(dec!(100.00)), Some(dec!(100.00)));
609        assert_eq!(tick.round_up(dec!(100.01)), Some(dec!(100.05)));
610        assert_eq!(tick.round_up(dec!(100.03)), Some(dec!(100.05)));
611        assert_eq!(tick.round_up(dec!(100.05)), Some(dec!(100.05)));
612
613        // In third threshold range
614        assert_eq!(tick.round_up(dec!(500.00)), Some(dec!(500.00)));
615        assert_eq!(tick.round_up(dec!(500.01)), Some(dec!(500.10)));
616        assert_eq!(tick.round_up(dec!(500.05)), Some(dec!(500.10)));
617        assert_eq!(tick.round_up(dec!(500.09)), Some(dec!(500.10)));
618        assert_eq!(tick.round_up(dec!(500.10)), Some(dec!(500.10)));
619    }
620
621    #[test]
622    fn test_round_down_varying() {
623        let tick = TickSize::Varying {
624            thresholds: vec![
625                (dec!(0), dec!(0.01)),
626                (dec!(100), dec!(0.05)),
627                (dec!(500), dec!(0.10)),
628            ],
629        };
630
631        // In first threshold range
632        assert_eq!(tick.round_down(dec!(50.00)), Some(dec!(50.00)));
633        assert_eq!(tick.round_down(dec!(50.001)), Some(dec!(50.00)));
634        assert_eq!(tick.round_down(dec!(99.999)), Some(dec!(99.99)));
635
636        // In second threshold range
637        assert_eq!(tick.round_down(dec!(100.00)), Some(dec!(100.00)));
638        assert_eq!(tick.round_down(dec!(100.01)), Some(dec!(100.00)));
639        assert_eq!(tick.round_down(dec!(100.04)), Some(dec!(100.00)));
640        assert_eq!(tick.round_down(dec!(100.05)), Some(dec!(100.05)));
641        assert_eq!(tick.round_down(dec!(100.09)), Some(dec!(100.05)));
642
643        // In third threshold range
644        assert_eq!(tick.round_down(dec!(500.00)), Some(dec!(500.00)));
645        assert_eq!(tick.round_down(dec!(500.01)), Some(dec!(500.00)));
646        assert_eq!(tick.round_down(dec!(500.09)), Some(dec!(500.00)));
647        assert_eq!(tick.round_down(dec!(500.10)), Some(dec!(500.10)));
648        assert_eq!(tick.round_down(dec!(500.19)), Some(dec!(500.10)));
649    }
650
651    #[test]
652    fn test_round_aggressive() {
653        let tick = TickSize::Simple(dec!(0.01));
654
655        // Buy direction - rounds up (more aggressive = higher price)
656        assert_eq!(tick.round_aggressive(dec!(100.001), Dir::Buy), Some(dec!(100.01)));
657        assert_eq!(tick.round_aggressive(dec!(100.00), Dir::Buy), Some(dec!(100.00)));
658
659        // Sell direction - rounds down (more aggressive = lower price)
660        assert_eq!(tick.round_aggressive(dec!(100.009), Dir::Sell), Some(dec!(100.00)));
661        assert_eq!(tick.round_aggressive(dec!(100.00), Dir::Sell), Some(dec!(100.00)));
662    }
663
664    #[test]
665    fn test_round_passive() {
666        let tick = TickSize::Simple(dec!(0.01));
667
668        // Buy direction - rounds down (more passive = lower price)
669        assert_eq!(tick.round_passive(dec!(100.009), Dir::Buy), Some(dec!(100.00)));
670        assert_eq!(tick.round_passive(dec!(100.00), Dir::Buy), Some(dec!(100.00)));
671
672        // Sell direction - rounds up (more passive = higher price)
673        assert_eq!(tick.round_passive(dec!(100.001), Dir::Sell), Some(dec!(100.01)));
674        assert_eq!(tick.round_passive(dec!(100.00), Dir::Sell), Some(dec!(100.00)));
675    }
676
677    #[test]
678    fn test_rounding_with_large_tick_sizes() {
679        // Test with tick size of 0.25
680        let tick = TickSize::Simple(dec!(0.25));
681
682        assert_eq!(tick.round_up(dec!(10.00)), Some(dec!(10.00)));
683        assert_eq!(tick.round_up(dec!(10.10)), Some(dec!(10.25)));
684        assert_eq!(tick.round_up(dec!(10.25)), Some(dec!(10.25)));
685        assert_eq!(tick.round_up(dec!(10.30)), Some(dec!(10.50)));
686
687        assert_eq!(tick.round_down(dec!(10.00)), Some(dec!(10.00)));
688        assert_eq!(tick.round_down(dec!(10.10)), Some(dec!(10.00)));
689        assert_eq!(tick.round_down(dec!(10.25)), Some(dec!(10.25)));
690        assert_eq!(tick.round_down(dec!(10.30)), Some(dec!(10.25)));
691        assert_eq!(tick.round_down(dec!(10.50)), Some(dec!(10.50)));
692        assert_eq!(tick.round_down(dec!(10.60)), Some(dec!(10.50)));
693    }
694}