1use std::{fmt::Display, str::FromStr, sync::LazyLock};
19
20use nautilus_core::correctness::{
21 CorrectnessError, CorrectnessResult, check_predicate_true, check_valid_string_ascii_optional,
22};
23use thiserror::Error;
24
25#[cfg(not(feature = "high-precision"))]
26use crate::types::fixed::f64_to_fixed_i64;
27#[cfg(feature = "high-precision")]
28use crate::types::fixed::f64_to_fixed_i128;
29use crate::types::{
30 Price,
31 fixed::{FIXED_PRECISION, FIXED_SCALAR},
32 price::{PRICE_MAX, PRICE_MIN, PRICE_RAW_MAX, PRICE_RAW_MIN, PriceRaw},
33};
34
35pub trait TickSchemeRule: Display {
36 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price>;
37 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price>;
38}
39
40#[derive(Clone, Debug, Error, PartialEq)]
42pub enum TickSchemeError {
43 #[error("tick must be finite")]
45 TickNotFinite {
46 tick: f64,
48 },
49 #[error("tick must be positive")]
51 TickNotPositive {
52 tick: f64,
54 },
55 #[error("tiers must not be empty")]
57 EmptyTiers,
58 #[error("tier {index}: values must not be NaN")]
60 TierValuesNaN {
61 index: usize,
63 start: f64,
65 stop: f64,
67 step: f64,
69 },
70 #[error("tier {index}: start ({start}) must be less than stop ({stop})")]
72 TierStartNotLessThanStop {
73 index: usize,
75 start: f64,
77 stop: f64,
79 },
80 #[error("tier {index}: step ({step}) must be positive")]
82 TierStepNotPositive {
83 index: usize,
85 step: f64,
87 },
88 #[error("tier {index}: step ({step}) must be less than range ({stop} - {start} = {range})")]
90 TierStepNotLessThanRange {
91 index: usize,
93 start: f64,
95 stop: f64,
97 step: f64,
99 range: f64,
101 },
102 #[error("tier {index}: start ({start}) overlaps previous tier stop ({prev_stop})")]
104 TierOverlapsPrevious {
105 index: usize,
107 start: f64,
109 prev_stop: f64,
111 },
112 #[error("tier {index}: start ({start}) outside Price range")]
114 TierStartOutsidePriceRange {
115 index: usize,
117 start: f64,
119 },
120 #[error("tier {index}: stop ({stop}) outside Price range")]
122 TierStopOutsidePriceRange {
123 index: usize,
125 stop: f64,
127 },
128 #[error("{source}")]
130 InvalidPrecision {
131 #[source]
133 source: CorrectnessError,
134 },
135 #[error("tier expansion produced no ticks")]
137 EmptyTickExpansion,
138 #[error("expanded tick value {value} outside Price range")]
140 ExpandedTickOutsidePriceRange {
141 value: f64,
143 },
144 #[error("unknown tick scheme {name}")]
146 UnknownName {
147 name: String,
149 },
150}
151
152pub const BETFAIR_TICK_SCHEME_NAME: &str = "BETFAIR";
153pub const TOPIX100_TICK_SCHEME_NAME: &str = "TOPIX100";
154pub const CRYPTO_0_01_TICK_SCHEME_NAME: &str = "CRYPTO_0_01";
155pub const FOREX_3DECIMAL_TICK_SCHEME_NAME: &str = "FOREX_3DECIMAL";
156pub const FOREX_5DECIMAL_TICK_SCHEME_NAME: &str = "FOREX_5DECIMAL";
157pub const FIXED_TICK_SCHEME_NAME: &str = "FIXED";
158pub const FIXED_PRECISION_TICK_SCHEME_PREFIX: &str = "FIXED_PRECISION_";
159
160const BETFAIR_PRICE_TIERS: [(f64, f64, f64); 10] = [
161 (1.01, 2.0, 0.01),
162 (2.0, 3.0, 0.02),
163 (3.0, 4.0, 0.05),
164 (4.0, 6.0, 0.1),
165 (6.0, 10.0, 0.2),
166 (10.0, 20.0, 0.5),
167 (20.0, 30.0, 1.0),
168 (30.0, 50.0, 2.0),
169 (50.0, 100.0, 5.0),
170 (100.0, 1010.0, 10.0),
171];
172
173pub static BETFAIR_TICK_SCHEME: LazyLock<TieredTickScheme> = LazyLock::new(|| {
174 TieredTickScheme::new(&BETFAIR_PRICE_TIERS, 2, 100)
175 .expect("BETFAIR tick scheme tiers are valid by construction")
176});
177
178pub static TOPIX100_TICK_SCHEME: LazyLock<TieredTickScheme> = LazyLock::new(|| {
179 TieredTickScheme::new(
180 &[
181 (0.1, 1_000.0, 0.1),
182 (1_000.0, 3_000.0, 0.5),
183 (3_000.0, 10_000.0, 1.0),
184 (10_000.0, 30_000.0, 5.0),
185 (30_000.0, 100_000.0, 10.0),
186 (100_000.0, 300_000.0, 50.0),
187 (300_000.0, 1_000_000.0, 100.0),
188 (1_000_000.0, 3_000_000.0, 500.0),
189 (3_000_000.0, 10_000_000.0, 1_000.0),
190 (10_000_000.0, 30_000_000.0, 5_000.0),
191 (30_000_000.0, f64::INFINITY, 10_000.0),
192 ],
193 4,
194 10_000,
195 )
196 .expect("TOPIX100 tick scheme tiers are valid by construction")
197});
198
199static FIXED_TICK_SCHEME: LazyLock<FixedTickScheme> =
200 LazyLock::new(|| FixedTickScheme::new(1.0).expect("fixed tick scheme is valid"));
201
202static CRYPTO_0_01_TICK_SCHEME: LazyLock<FixedTickScheme> =
203 LazyLock::new(|| FixedTickScheme::new(0.01).expect("crypto tick scheme is valid"));
204
205static FIXED_PRECISION_TICK_SCHEMES: LazyLock<Vec<FixedTickScheme>> = LazyLock::new(|| {
206 (0..=FIXED_PRECISION)
207 .map(|precision| {
208 let tick = 10_f64.powi(-i32::from(precision));
209 FixedTickScheme::new(tick).expect("fixed precision tick scheme is valid")
210 })
211 .collect()
212});
213
214#[derive(Clone, Copy, Debug)]
215pub struct FixedTickScheme {
216 tick: f64,
217}
218
219impl PartialEq for FixedTickScheme {
220 fn eq(&self, other: &Self) -> bool {
221 self.tick == other.tick
222 }
223}
224impl Eq for FixedTickScheme {}
225
226impl FixedTickScheme {
227 pub fn new(tick: f64) -> Result<Self, TickSchemeError> {
233 if !tick.is_finite() {
234 return Err(TickSchemeError::TickNotFinite { tick });
235 }
236
237 if tick <= 0.0 {
238 return Err(TickSchemeError::TickNotPositive { tick });
239 }
240
241 Ok(Self { tick })
242 }
243}
244
245impl TickSchemeRule for FixedTickScheme {
246 #[inline(always)]
247 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
248 fixed_next_bid_price(self.tick, value, n, precision)
249 }
250
251 #[inline(always)]
252 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
253 fixed_next_ask_price(self.tick, value, n, precision)
254 }
255}
256
257impl Display for FixedTickScheme {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 write!(f, "FIXED")
260 }
261}
262
263#[derive(Clone, Debug)]
269pub struct TieredTickScheme {
270 ticks: Vec<PriceRaw>,
271 precision: u8,
272}
273
274impl PartialEq for TieredTickScheme {
275 fn eq(&self, other: &Self) -> bool {
276 self.precision == other.precision && self.ticks == other.ticks
277 }
278}
279impl Eq for TieredTickScheme {}
280
281impl TieredTickScheme {
282 pub fn new(
291 tiers: &[(f64, f64, f64)],
292 price_precision: u8,
293 max_ticks_per_tier: usize,
294 ) -> Result<Self, TickSchemeError> {
295 if tiers.is_empty() {
296 return Err(TickSchemeError::EmptyTiers);
297 }
298
299 for (index, &(start, stop, step)) in tiers.iter().enumerate() {
300 if start.is_nan() || stop.is_nan() || step.is_nan() {
301 return Err(TickSchemeError::TierValuesNaN {
302 index,
303 start,
304 stop,
305 step,
306 });
307 }
308
309 if start >= stop {
310 return Err(TickSchemeError::TierStartNotLessThanStop { index, start, stop });
311 }
312
313 if step <= 0.0 {
314 return Err(TickSchemeError::TierStepNotPositive { index, step });
315 }
316
317 if !stop.is_infinite() && step >= (stop - start) {
318 return Err(TickSchemeError::TierStepNotLessThanRange {
319 index,
320 start,
321 stop,
322 step,
323 range: stop - start,
324 });
325 }
326
327 if index > 0 {
328 let prev_stop = tiers[index - 1].1;
329
330 if start < prev_stop {
331 return Err(TickSchemeError::TierOverlapsPrevious {
332 index,
333 start,
334 prev_stop,
335 });
336 }
337 }
338
339 if !(PRICE_MIN..=PRICE_MAX).contains(&start) {
340 return Err(TickSchemeError::TierStartOutsidePriceRange { index, start });
341 }
342
343 if !stop.is_infinite() && !(PRICE_MIN..=PRICE_MAX).contains(&stop) {
344 return Err(TickSchemeError::TierStopOutsidePriceRange { index, stop });
345 }
346 }
347
348 let _ = Price::new_checked(0.0, price_precision)
349 .map_err(|source| TickSchemeError::InvalidPrecision { source })?;
350
351 let ticks = Self::build_ticks(tiers, price_precision, max_ticks_per_tier)?;
352
353 if ticks.is_empty() {
354 return Err(TickSchemeError::EmptyTickExpansion);
355 }
356 Ok(Self {
357 ticks,
358 precision: price_precision,
359 })
360 }
361
362 fn build_ticks(
363 tiers: &[(f64, f64, f64)],
364 precision: u8,
365 max_ticks_per_tier: usize,
366 ) -> Result<Vec<PriceRaw>, TickSchemeError> {
367 let mut all_ticks = Vec::new();
368
369 for &(start, stop, step) in tiers {
370 let effective_stop = if stop.is_infinite() {
371 start + (max_ticks_per_tier.saturating_add(1) as f64) * step
372 } else {
373 stop
374 };
375 let mut i = 0;
376 while i < max_ticks_per_tier {
377 let value = start + (i as f64) * step;
378
379 if value >= effective_stop {
380 break;
381 }
382
383 if !value.is_finite() || !(PRICE_MIN..=PRICE_MAX).contains(&value) {
384 return Err(TickSchemeError::ExpandedTickOutsidePriceRange { value });
385 }
386 let raw = f64_to_raw(value, precision);
387
388 if all_ticks.last() != Some(&raw) {
389 all_ticks.push(raw);
390 }
391 i += 1;
392 }
393 }
394 Ok(all_ticks)
395 }
396
397 #[inline(always)]
398 fn price_at(&self, index: usize) -> Price {
399 Price {
400 raw: self.ticks[index],
401 precision: self.precision,
402 }
403 }
404
405 #[must_use]
407 pub fn ticks(&self) -> Vec<Price> {
408 self.ticks
409 .iter()
410 .map(|&raw| Price {
411 raw,
412 precision: self.precision,
413 })
414 .collect()
415 }
416
417 #[must_use]
419 pub fn tick_count(&self) -> usize {
420 self.ticks.len()
421 }
422
423 #[must_use]
425 pub fn min_price(&self) -> Price {
426 self.price_at(0)
427 }
428
429 #[must_use]
431 pub fn max_price(&self) -> Price {
432 self.price_at(self.ticks.len() - 1)
433 }
434
435 #[must_use]
437 pub fn precision(&self) -> u8 {
438 self.precision
439 }
440
441 #[must_use]
447 pub fn topix100() -> Self {
448 TOPIX100_TICK_SCHEME.clone()
449 }
450
451 #[must_use]
457 pub fn betfair() -> Self {
458 BETFAIR_TICK_SCHEME.clone()
459 }
460}
461
462impl TickSchemeRule for TieredTickScheme {
463 fn next_bid_price(&self, value: f64, n: i32, _precision: u8) -> Option<Price> {
464 let n = usize::try_from(n).ok()?;
465
466 if value.is_nan() {
467 return None;
468 }
469
470 let raw_floor = (value * FIXED_SCALAR).floor() as PriceRaw;
473
474 if raw_floor < self.ticks[0] {
475 return None;
476 }
477
478 let idx = self.ticks.partition_point(|&t| t < raw_floor);
480
481 if idx < self.ticks.len() && self.ticks[idx] == raw_floor {
482 let target = idx.checked_sub(n)?;
484 return Some(self.price_at(target));
485 }
486
487 let target = idx.checked_sub(1)?.checked_sub(n)?;
489 Some(self.price_at(target))
490 }
491
492 fn next_ask_price(&self, value: f64, n: i32, _precision: u8) -> Option<Price> {
493 let n = usize::try_from(n).ok()?;
494
495 if value.is_nan() {
496 return None;
497 }
498
499 let raw_ceil = (value * FIXED_SCALAR).ceil() as PriceRaw;
502
503 if raw_ceil > *self.ticks.last()? {
504 return None;
505 }
506
507 let idx = self.ticks.partition_point(|&t| t < raw_ceil);
509 let target = idx.checked_add(n)?;
510
511 if target >= self.ticks.len() {
512 return None;
513 }
514 Some(self.price_at(target))
515 }
516}
517
518impl Display for TieredTickScheme {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 write!(f, "TIERED")
521 }
522}
523
524#[derive(Clone, Debug, PartialEq, Eq)]
525pub enum TickScheme {
526 Fixed(FixedTickScheme),
527 Tiered(TieredTickScheme),
528 Betfair,
529 Crypto,
530}
531
532impl TickSchemeRule for TickScheme {
533 #[inline(always)]
534 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
535 match self {
536 Self::Fixed(scheme) => scheme.next_bid_price(value, n, precision),
537 Self::Tiered(scheme) => scheme.next_bid_price(value, n, precision),
538 Self::Betfair => BETFAIR_TICK_SCHEME.next_bid_price(value, n, precision),
539 Self::Crypto => CRYPTO_0_01_TICK_SCHEME.next_bid_price(value, n, precision),
540 }
541 }
542
543 #[inline(always)]
544 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
545 match self {
546 Self::Fixed(scheme) => scheme.next_ask_price(value, n, precision),
547 Self::Tiered(scheme) => scheme.next_ask_price(value, n, precision),
548 Self::Betfair => BETFAIR_TICK_SCHEME.next_ask_price(value, n, precision),
549 Self::Crypto => CRYPTO_0_01_TICK_SCHEME.next_ask_price(value, n, precision),
550 }
551 }
552}
553
554impl Display for TickScheme {
555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556 match self {
557 Self::Fixed(_) => write!(f, "FIXED"),
558 Self::Tiered(scheme) => write!(f, "{scheme}"),
559 Self::Betfair => write!(f, "{BETFAIR_TICK_SCHEME_NAME}"),
560 Self::Crypto => write!(f, "{CRYPTO_0_01_TICK_SCHEME_NAME}"),
561 }
562 }
563}
564
565impl FromStr for TickScheme {
566 type Err = TickSchemeError;
567
568 fn from_str(s: &str) -> Result<Self, Self::Err> {
569 match s.trim().to_ascii_uppercase().as_str() {
570 FIXED_TICK_SCHEME_NAME => Ok(Self::Fixed(FixedTickScheme::new(1.0)?)),
571 FOREX_3DECIMAL_TICK_SCHEME_NAME => Ok(Self::Fixed(FixedTickScheme::new(0.001)?)),
572 FOREX_5DECIMAL_TICK_SCHEME_NAME => Ok(Self::Fixed(FixedTickScheme::new(0.00001)?)),
573 TOPIX100_TICK_SCHEME_NAME => Ok(Self::Tiered(TieredTickScheme::topix100())),
574 BETFAIR_TICK_SCHEME_NAME => Ok(Self::Betfair),
575 CRYPTO_0_01_TICK_SCHEME_NAME => Ok(Self::Crypto),
576 name => {
577 if let Some(precision) = parse_fixed_precision_name(name)
578 && precision <= FIXED_PRECISION
579 {
580 let tick = 10_f64.powi(-i32::from(precision));
581 return Ok(Self::Fixed(FixedTickScheme::new(tick)?));
582 }
583 Err(TickSchemeError::UnknownName {
584 name: s.to_string(),
585 })
586 }
587 }
588 }
589}
590
591#[must_use]
593pub fn tick_scheme_rule_from_name(name: &str) -> Option<&'static dyn TickSchemeRule> {
594 let name = name.trim();
595 if name.eq_ignore_ascii_case(FIXED_TICK_SCHEME_NAME) {
596 Some(&*FIXED_TICK_SCHEME)
597 } else if name.eq_ignore_ascii_case(FOREX_3DECIMAL_TICK_SCHEME_NAME) {
598 Some(&FIXED_PRECISION_TICK_SCHEMES[3])
599 } else if name.eq_ignore_ascii_case(FOREX_5DECIMAL_TICK_SCHEME_NAME) {
600 Some(&FIXED_PRECISION_TICK_SCHEMES[5])
601 } else if name.eq_ignore_ascii_case(TOPIX100_TICK_SCHEME_NAME) {
602 Some(&*TOPIX100_TICK_SCHEME)
603 } else if name.eq_ignore_ascii_case(BETFAIR_TICK_SCHEME_NAME) {
604 Some(&*BETFAIR_TICK_SCHEME)
605 } else if name.eq_ignore_ascii_case(CRYPTO_0_01_TICK_SCHEME_NAME) {
606 Some(&*CRYPTO_0_01_TICK_SCHEME)
607 } else {
608 parse_fixed_precision_name_ignore_ascii_case(name).and_then(|precision| {
609 FIXED_PRECISION_TICK_SCHEMES
610 .get(usize::from(precision))
611 .map(|scheme| scheme as &dyn TickSchemeRule)
612 })
613 }
614}
615
616pub fn check_tick_scheme<T: AsRef<str> + Copy>(tick_scheme: Option<T>) -> CorrectnessResult<()> {
622 check_valid_string_ascii_optional(tick_scheme, "tick_scheme")?;
623 if let Some(name) = tick_scheme {
624 check_predicate_true(
625 tick_scheme_rule_from_name(name.as_ref()).is_some(),
626 "tick_scheme not found in tick schemes",
627 )?;
628 }
629 Ok(())
630}
631
632#[inline(always)]
634fn f64_to_raw(value: f64, precision: u8) -> PriceRaw {
635 #[cfg(feature = "high-precision")]
636 {
637 f64_to_fixed_i128(value, precision)
638 }
639 #[cfg(not(feature = "high-precision"))]
640 {
641 f64_to_fixed_i64(value, precision)
642 }
643}
644
645fn parse_fixed_precision_name(name: &str) -> Option<u8> {
646 name.strip_prefix(FIXED_PRECISION_TICK_SCHEME_PREFIX)
647 .and_then(|precision| precision.parse::<u8>().ok())
648}
649
650fn parse_fixed_precision_name_ignore_ascii_case(name: &str) -> Option<u8> {
651 let prefix_len = FIXED_PRECISION_TICK_SCHEME_PREFIX.len();
652 let prefix = name.get(..prefix_len)?;
653 if !prefix.eq_ignore_ascii_case(FIXED_PRECISION_TICK_SCHEME_PREFIX) {
654 return None;
655 }
656
657 name.get(prefix_len..)?.parse::<u8>().ok()
658}
659
660fn fixed_next_bid_price(tick: f64, value: f64, n: i32, precision: u8) -> Option<Price> {
661 let n = PriceRaw::from(n);
662 if n < 0 {
663 return None;
664 }
665 let tick_raw = fixed_tick_raw(tick, precision)?;
666 let value_raw = value_to_raw(value)?;
667 let base = value_raw
668 .checked_div_euclid(tick_raw)?
669 .checked_mul(tick_raw)?;
670 let offset = tick_raw.checked_mul(n)?;
671 price_from_raw_checked(base.checked_sub(offset)?, precision)
672}
673
674fn fixed_next_ask_price(tick: f64, value: f64, n: i32, precision: u8) -> Option<Price> {
675 let n = PriceRaw::from(n);
676 if n < 0 {
677 return None;
678 }
679 let tick_raw = fixed_tick_raw(tick, precision)?;
680 let value_raw = value_to_raw(value)?;
681 let base = value_raw
682 .checked_neg()?
683 .checked_div_euclid(tick_raw)?
684 .checked_neg()?
685 .checked_mul(tick_raw)?;
686 let offset = tick_raw.checked_mul(n)?;
687 price_from_raw_checked(base.checked_add(offset)?, precision)
688}
689
690fn fixed_tick_raw(tick: f64, precision: u8) -> Option<PriceRaw> {
691 Price::new_checked(0.0, precision).ok()?;
692
693 if !tick.is_finite() || tick <= 0.0 {
694 return None;
695 }
696
697 let raw = f64_to_raw(tick, precision);
698 (raw > 0).then_some(raw)
699}
700
701fn value_to_raw(value: f64) -> Option<PriceRaw> {
702 if !value.is_finite() || !(PRICE_MIN..=PRICE_MAX).contains(&value) {
703 return None;
704 }
705 Some(f64_to_raw(value, FIXED_PRECISION))
706}
707
708fn price_from_raw_checked(raw: PriceRaw, precision: u8) -> Option<Price> {
709 if !(PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(&raw) {
710 return None;
711 }
712 Some(Price { raw, precision })
713}
714
715#[cfg(test)]
716mod tests {
717 use std::str::FromStr;
718
719 use proptest::prelude::*;
720 use rstest::rstest;
721
722 use super::*;
723
724 #[rstest]
725 fn fixed_tick_scheme_prices() {
726 let scheme = FixedTickScheme::new(0.5).unwrap();
727 let bid = scheme.next_bid_price(10.3, 0, 2).unwrap();
728 let ask = scheme.next_ask_price(10.3, 0, 2).unwrap();
729 assert!(bid < ask);
730 }
731
732 #[rstest]
733 fn fixed_tick_negative_returns_typed_error_with_display() {
734 let error = FixedTickScheme::new(-0.01).unwrap_err();
735
736 assert_eq!(error, TickSchemeError::TickNotPositive { tick: -0.01 });
737 assert_eq!(error.to_string(), "tick must be positive");
738 }
739
740 #[rstest]
741 fn fixed_tick_boundary() {
742 let scheme = FixedTickScheme::new(0.5).unwrap();
743 let price = scheme.next_bid_price(10.5, 0, 2).unwrap();
744 assert_eq!(price, Price::new(10.5, 2));
745 }
746
747 #[rstest]
748 fn fixed_tick_scheme_preserves_decimal_boundaries() {
749 let tenth = FixedTickScheme::new(0.1).unwrap();
750 let cent = FixedTickScheme::new(0.01).unwrap();
751
752 assert_eq!(tenth.next_bid_price(0.3, 0, 1), Some(Price::new(0.3, 1)));
753 assert_eq!(tenth.next_ask_price(0.3, 0, 1), Some(Price::new(0.3, 1)));
754 assert_eq!(cent.next_bid_price(0.07, 0, 2), Some(Price::new(0.07, 2)));
755 assert_eq!(cent.next_ask_price(0.07, 0, 2), Some(Price::new(0.07, 2)));
756 }
757
758 #[rstest]
759 fn fixed_tick_multiple_steps() {
760 let scheme = FixedTickScheme::new(1.0).unwrap();
761 let bid = scheme.next_bid_price(10.0, 2, 1).unwrap();
762 let ask = scheme.next_ask_price(10.0, 3, 1).unwrap();
763 assert_eq!(bid, Price::new(8.0, 1));
764 assert_eq!(ask, Price::new(13.0, 1));
765 }
766
767 #[rstest]
768 fn tick_scheme_round_trip() {
769 let scheme = TickScheme::from_str("CRYPTO_0_01").unwrap();
770 assert_eq!(scheme.to_string(), "CRYPTO_0_01");
771 }
772
773 #[rstest]
774 fn tick_scheme_rule_from_fixed_precision_name() {
775 let scheme = tick_scheme_rule_from_name("fixed_precision_1").unwrap();
776
777 assert_eq!(scheme.next_bid_price(0.3, 0, 1), Some(Price::new(0.3, 1)));
778 assert_eq!(scheme.next_ask_price(0.31, 0, 1), Some(Price::new(0.4, 1)));
779 }
780
781 #[rstest]
782 fn tick_scheme_unknown() {
783 let error = TickScheme::from_str("UNKNOWN").unwrap_err();
784
785 assert_eq!(
786 error,
787 TickSchemeError::UnknownName {
788 name: "UNKNOWN".to_string(),
789 }
790 );
791 assert_eq!(error.to_string(), "unknown tick scheme UNKNOWN");
792 }
793
794 #[rstest]
795 fn tick_scheme_fixed_precision_above_max_returns_unknown_name() {
796 let name = format!("FIXED_PRECISION_{}", FIXED_PRECISION + 1);
797 let error = TickScheme::from_str(&name).unwrap_err();
798
799 assert_eq!(error, TickSchemeError::UnknownName { name: name.clone() });
800 assert_eq!(error.to_string(), format!("unknown tick scheme {name}"));
801 }
802
803 #[rstest]
804 fn fixed_tick_zero() {
805 let error = FixedTickScheme::new(0.0).unwrap_err();
806
807 assert_eq!(error, TickSchemeError::TickNotPositive { tick: 0.0 });
808 assert_eq!(error.to_string(), "tick must be positive");
809 }
810
811 #[rstest]
812 #[case(f64::INFINITY)]
813 #[case(f64::NAN)]
814 fn fixed_tick_non_finite_returns_error(#[case] tick: f64) {
815 let error = FixedTickScheme::new(tick).unwrap_err();
816
817 match &error {
818 TickSchemeError::TickNotFinite {
819 tick: returned_tick,
820 } => {
821 assert!(
822 *returned_tick == tick || returned_tick.is_nan() && tick.is_nan(),
823 "returned tick {returned_tick} did not match input {tick}",
824 );
825 }
826 _ => panic!("unexpected error variant: {error:?}"),
827 }
828 assert_eq!(error.to_string(), "tick must be finite");
829 }
830
831 #[rstest]
832 fn fixed_tick_scheme_nan_value_returns_none() {
833 let scheme = FixedTickScheme::new(1.0).unwrap();
834 assert!(scheme.next_bid_price(f64::NAN, 0, 2).is_none());
835 assert!(scheme.next_ask_price(f64::NAN, 0, 2).is_none());
836 }
837
838 #[rstest]
839 fn fixed_tick_scheme_out_of_range_returns_none() {
840 let scheme = FixedTickScheme::new(PRICE_MAX).unwrap();
842 assert!(scheme.next_ask_price(PRICE_MAX, 1, 2).is_none());
843 }
844
845 #[rstest]
846 fn tiered_tick_scheme_topix100_construction() {
847 let scheme = TieredTickScheme::topix100();
848 assert!(scheme.tick_count() > 0);
849 assert_eq!(scheme.precision(), 4);
850 assert_eq!(scheme.min_price(), Price::new(0.1, 4));
851 }
852
853 #[rstest]
854 fn tiered_tick_scheme_betfair_construction() {
855 let scheme = TieredTickScheme::betfair();
856 assert_eq!(scheme.tick_count(), 350);
857 assert_eq!(scheme.precision(), 2);
858 assert_eq!(scheme.min_price(), Price::new(1.01, 2));
859 assert_eq!(scheme.max_price(), Price::new(1000.0, 2));
860 }
861
862 #[rstest]
863 fn tiered_tick_scheme_ask_at_low_price() {
864 let scheme = TieredTickScheme::topix100();
865 let ask = scheme.next_ask_price(500.0, 0, 4).unwrap();
866 assert_eq!(ask, Price::new(500.0, 4));
867 }
868
869 #[rstest]
870 fn tiered_tick_scheme_bid_at_low_price() {
871 let scheme = TieredTickScheme::topix100();
872 let bid = scheme.next_bid_price(500.0, 0, 4).unwrap();
873 assert_eq!(bid, Price::new(500.0, 4));
874 }
875
876 #[rstest]
877 fn tiered_tick_scheme_ask_steps() {
878 let scheme = TieredTickScheme::topix100();
879 let ask0 = scheme.next_ask_price(500.0, 0, 4).unwrap();
880 let ask1 = scheme.next_ask_price(500.0, 1, 4).unwrap();
881 assert!(ask1 > ask0);
882 assert_eq!(ask1, Price::new(500.1, 4));
883 }
884
885 #[rstest]
886 fn tiered_tick_scheme_bid_steps() {
887 let scheme = TieredTickScheme::topix100();
888 let bid0 = scheme.next_bid_price(500.0, 0, 4).unwrap();
889 let bid1 = scheme.next_bid_price(500.0, 1, 4).unwrap();
890 assert!(bid1 < bid0);
891 assert_eq!(bid1, Price::new(499.9, 4));
892 }
893
894 #[rstest]
895 fn tiered_tick_scheme_tier_boundary_1000() {
896 let scheme = TieredTickScheme::topix100();
897 let ask = scheme.next_ask_price(1000.0, 1, 4).unwrap();
899 assert_eq!(ask, Price::new(1000.5, 4));
900 }
901
902 #[rstest]
903 #[case(3.90, 1, "3.95")]
904 #[case(4.0, 1, "4.10")]
905 fn tiered_tick_scheme_betfair_ask_transition(
906 #[case] value: f64,
907 #[case] n: i32,
908 #[case] expected: &str,
909 ) {
910 let scheme = TieredTickScheme::betfair();
911 let ask = scheme.next_ask_price(value, n, 2).unwrap();
912 assert_eq!(ask, Price::from(expected));
913 }
914
915 #[rstest]
916 #[case(1.499, 0, "1.49")]
917 #[case(2.011, 0, "2.00")]
918 #[case(2.027, 2, "1.99")]
919 fn tiered_tick_scheme_betfair_bid_transition(
920 #[case] value: f64,
921 #[case] n: i32,
922 #[case] expected: &str,
923 ) {
924 let scheme = TieredTickScheme::betfair();
925 let bid = scheme.next_bid_price(value, n, 2).unwrap();
926 assert_eq!(bid, Price::from(expected));
927 }
928
929 #[rstest]
930 fn tiered_tick_scheme_between_ticks() {
931 let scheme = TieredTickScheme::topix100();
932 let ask = scheme.next_ask_price(1000.3, 0, 4).unwrap();
934 assert!(ask.as_f64() >= 1000.3);
935 let bid = scheme.next_bid_price(1000.3, 0, 4).unwrap();
936 assert!(bid.as_f64() <= 1000.3);
937 }
938
939 #[rstest]
940 fn tiered_tick_scheme_off_grid_bid_below_tick() {
941 let scheme = TieredTickScheme::new(&[(1.0, 2.0, 0.05)], 2, 100).unwrap();
943 let bid = scheme.next_bid_price(1.049, 0, 2).unwrap();
944 assert_eq!(bid, Price::new(1.0, 2));
945 }
946
947 #[rstest]
948 fn tiered_tick_scheme_off_grid_ask_above_tick() {
949 let scheme = TieredTickScheme::new(&[(1.0, 2.0, 0.05)], 2, 100).unwrap();
951 let ask = scheme.next_ask_price(1.051, 0, 2).unwrap();
952 assert_eq!(ask, Price::new(1.10, 2));
953 }
954
955 #[rstest]
956 fn tiered_tick_scheme_bid_below_min_returns_none() {
957 let scheme = TieredTickScheme::topix100();
958 assert!(scheme.next_bid_price(0.05, 0, 4).is_none());
959 }
960
961 #[rstest]
962 fn tiered_tick_scheme_ask_beyond_last_tick_returns_none() {
963 let scheme = TieredTickScheme::topix100();
964 let last = scheme.max_price().as_f64();
965 assert!(scheme.next_ask_price(last, 1, 4).is_none());
966 }
967
968 #[rstest]
969 fn tiered_tick_scheme_bid_beyond_last_tick_returns_last() {
970 let scheme = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], 1, 100).unwrap();
971 let bid = scheme.next_bid_price(9.5, 0, 1).unwrap();
973 assert_eq!(bid, Price::new(9.0, 1));
974 }
975
976 #[rstest]
977 fn tiered_tick_scheme_negative_n_returns_none() {
978 let scheme = TieredTickScheme::topix100();
979 assert!(scheme.next_bid_price(500.0, -1, 4).is_none());
980 assert!(scheme.next_ask_price(500.0, -1, 4).is_none());
981 }
982
983 #[rstest]
984 fn tiered_tick_scheme_nan_value_returns_none() {
985 let scheme = TieredTickScheme::topix100();
986 assert!(scheme.next_bid_price(f64::NAN, 0, 4).is_none());
987 assert!(scheme.next_ask_price(f64::NAN, 0, 4).is_none());
988 }
989
990 #[rstest]
991 fn tiered_tick_scheme_infinite_value_saturates() {
992 let scheme = TieredTickScheme::topix100();
993 assert_eq!(
994 scheme.next_bid_price(f64::INFINITY, 0, 4),
995 Some(scheme.max_price())
996 );
997 assert!(scheme.next_ask_price(f64::INFINITY, 0, 4).is_none());
998 assert!(scheme.next_bid_price(f64::NEG_INFINITY, 0, 4).is_none());
999 }
1000
1001 #[rstest]
1002 fn crypto_tick_scheme_out_of_range_returns_none() {
1003 let scheme = TickScheme::Crypto;
1005 assert!(scheme.next_ask_price(PRICE_MAX * 2.0, 0, 2).is_none());
1006 assert!(scheme.next_bid_price(PRICE_MIN * 2.0, 0, 2).is_none());
1007 }
1008
1009 #[rstest]
1010 fn tiered_tick_scheme_validation_empty_tiers() {
1011 let error = TieredTickScheme::new(&[], 2, 100).unwrap_err();
1012
1013 assert_eq!(error, TickSchemeError::EmptyTiers);
1014 assert_eq!(error.to_string(), "tiers must not be empty");
1015 }
1016
1017 #[rstest]
1018 #[case(
1019 vec![(100.0, 50.0, 1.0)],
1020 TickSchemeError::TierStartNotLessThanStop {
1021 index: 0,
1022 start: 100.0,
1023 stop: 50.0,
1024 },
1025 "tier 0: start (100) must be less than stop (50)"
1026 )]
1027 #[case(
1028 vec![(2.0, 2.0, 0.1)],
1029 TickSchemeError::TierStartNotLessThanStop {
1030 index: 0,
1031 start: 2.0,
1032 stop: 2.0,
1033 },
1034 "tier 0: start (2) must be less than stop (2)"
1035 )]
1036 #[case(
1037 vec![(0.0, 100.0, -1.0)],
1038 TickSchemeError::TierStepNotPositive {
1039 index: 0,
1040 step: -1.0,
1041 },
1042 "tier 0: step (-1) must be positive"
1043 )]
1044 #[case(
1045 vec![(1.0, 2.0, 0.0)],
1046 TickSchemeError::TierStepNotPositive {
1047 index: 0,
1048 step: 0.0,
1049 },
1050 "tier 0: step (0) must be positive"
1051 )]
1052 #[case(
1053 vec![(0.0, 100.0, 200.0)],
1054 TickSchemeError::TierStepNotLessThanRange {
1055 index: 0,
1056 start: 0.0,
1057 stop: 100.0,
1058 step: 200.0,
1059 range: 100.0,
1060 },
1061 "tier 0: step (200) must be less than range (100 - 0 = 100)"
1062 )]
1063 #[case(
1064 vec![(10.0, 20.0, 1.0), (1.0, 10.0, 1.0)],
1065 TickSchemeError::TierOverlapsPrevious {
1066 index: 1,
1067 start: 1.0,
1068 prev_stop: 20.0,
1069 },
1070 "tier 1: start (1) overlaps previous tier stop (20)"
1071 )]
1072 #[case(
1073 vec![(1.0, 10.0, 1.0), (5.0, 15.0, 1.0)],
1074 TickSchemeError::TierOverlapsPrevious {
1075 index: 1,
1076 start: 5.0,
1077 prev_stop: 10.0,
1078 },
1079 "tier 1: start (5) overlaps previous tier stop (10)"
1080 )]
1081 fn tiered_tick_scheme_invalid_tiers_return_typed_errors(
1082 #[case] tiers: Vec<(f64, f64, f64)>,
1083 #[case] expected_error: TickSchemeError,
1084 #[case] expected_display: &str,
1085 ) {
1086 let error = TieredTickScheme::new(&tiers, 2, 100).unwrap_err();
1087
1088 assert_eq!(error, expected_error);
1089 assert_eq!(error.to_string(), expected_display);
1090 }
1091
1092 #[rstest]
1093 #[case(vec![(f64::NAN, 10.0, 1.0)])]
1094 #[case(vec![(1.0, f64::NAN, 1.0)])]
1095 #[case(vec![(1.0, 10.0, f64::NAN)])]
1096 fn tiered_tick_scheme_nan_tiers_return_typed_error(#[case] tiers: Vec<(f64, f64, f64)>) {
1097 let error = TieredTickScheme::new(&tiers, 2, 100).unwrap_err();
1098
1099 match &error {
1100 TickSchemeError::TierValuesNaN {
1101 index,
1102 start,
1103 stop,
1104 step,
1105 } => {
1106 assert_eq!(*index, 0);
1107 assert!(
1108 start.is_nan() || stop.is_nan() || step.is_nan(),
1109 "expected one NaN tier value in {error:?}",
1110 );
1111 }
1112 _ => panic!("unexpected error variant: {error:?}"),
1113 }
1114 assert_eq!(error.to_string(), "tier 0: values must not be NaN");
1115 }
1116
1117 #[rstest]
1118 fn tiered_tick_scheme_start_outside_price_range_returns_typed_error() {
1119 let start = PRICE_MIN - 1.0;
1120 let stop = PRICE_MIN + 1.0;
1121 let error = TieredTickScheme::new(&[(start, stop, 1.0)], 2, 100).unwrap_err();
1122
1123 assert_eq!(
1124 error,
1125 TickSchemeError::TierStartOutsidePriceRange { index: 0, start }
1126 );
1127 assert_eq!(
1128 error.to_string(),
1129 format!("tier 0: start ({start}) outside Price range")
1130 );
1131 }
1132
1133 #[rstest]
1134 fn tiered_tick_scheme_stop_outside_price_range_returns_typed_error() {
1135 let start = PRICE_MAX - 2.0;
1136 let stop = PRICE_MAX + 1.0;
1137 let error = TieredTickScheme::new(&[(start, stop, 1.0)], 2, 100).unwrap_err();
1138
1139 assert_eq!(
1140 error,
1141 TickSchemeError::TierStopOutsidePriceRange { index: 0, stop }
1142 );
1143 assert_eq!(
1144 error.to_string(),
1145 format!("tier 0: stop ({stop}) outside Price range")
1146 );
1147 }
1148
1149 #[rstest]
1150 fn tiered_tick_scheme_invalid_precision_wraps_source_error() {
1151 let invalid_precision = FIXED_PRECISION + 1;
1152 let source = Price::new_checked(0.0, invalid_precision).unwrap_err();
1153 let error = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], invalid_precision, 100).unwrap_err();
1154
1155 assert_eq!(
1156 error,
1157 TickSchemeError::InvalidPrecision {
1158 source: source.clone(),
1159 }
1160 );
1161 assert_eq!(error.to_string(), source.to_string());
1162 }
1163
1164 #[rstest]
1165 fn tiered_tick_scheme_empty_expansion_returns_typed_error() {
1166 let error = TieredTickScheme::new(&[(1.0, f64::INFINITY, 1.0)], 2, 0).unwrap_err();
1167
1168 assert_eq!(error, TickSchemeError::EmptyTickExpansion);
1169 assert_eq!(error.to_string(), "tier expansion produced no ticks");
1170 }
1171
1172 #[rstest]
1173 fn tiered_tick_scheme_expanded_tick_outside_range_returns_typed_error() {
1174 let invalid_value = PRICE_MAX + 1.0;
1175 let error = TieredTickScheme::new(&[(PRICE_MAX, f64::INFINITY, 1.0)], 2, 2).unwrap_err();
1176
1177 assert_eq!(
1178 error,
1179 TickSchemeError::ExpandedTickOutsidePriceRange {
1180 value: invalid_value,
1181 }
1182 );
1183 assert_eq!(
1184 error.to_string(),
1185 format!("expanded tick value {invalid_value} outside Price range")
1186 );
1187 }
1188
1189 #[rstest]
1190 fn tiered_tick_scheme_finite_tier_includes_all_ticks() {
1191 let scheme = TieredTickScheme::new(&[(0.0, 0.3, 0.1)], 1, 100).unwrap();
1193 assert_eq!(scheme.tick_count(), 3);
1194 }
1195
1196 #[rstest]
1197 fn tiered_tick_scheme_simple_two_tiers() {
1198 let scheme =
1199 TieredTickScheme::new(&[(1.0, 10.0, 1.0), (10.0, 100.0, 5.0)], 2, 100).unwrap();
1200 let ticks = scheme.ticks();
1201 assert_eq!(ticks[0], Price::new(1.0, 2));
1204 assert_eq!(ticks[8], Price::new(9.0, 2));
1205 assert_eq!(ticks[9], Price::new(10.0, 2));
1206 assert_eq!(ticks[10], Price::new(15.0, 2));
1207 }
1208
1209 #[rstest]
1210 fn tiered_tick_scheme_infinity_tier() {
1211 let scheme = TieredTickScheme::new(&[(100.0, f64::INFINITY, 10.0)], 1, 5).unwrap();
1212 assert_eq!(scheme.tick_count(), 5);
1213 let ticks = scheme.ticks();
1214 assert_eq!(ticks[0], Price::new(100.0, 1));
1215 assert_eq!(ticks[4], Price::new(140.0, 1));
1216 }
1217
1218 #[rstest]
1219 fn tiered_tick_scheme_from_str_topix100() {
1220 let scheme = TickScheme::from_str("TOPIX100").unwrap();
1221 assert_eq!(scheme.to_string(), "TIERED");
1222 }
1223
1224 #[rstest]
1225 fn tiered_tick_scheme_from_str_betfair() {
1226 let scheme = TickScheme::from_str("BETFAIR").unwrap();
1227 assert_eq!(scheme.to_string(), BETFAIR_TICK_SCHEME_NAME);
1228 assert_eq!(
1229 scheme.next_ask_price(4.0, 1, 2).unwrap(),
1230 Price::new(4.1, 2)
1231 );
1232 }
1233
1234 #[rstest]
1235 fn tiered_tick_scheme_display() {
1236 let scheme = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], 2, 100).unwrap();
1237 assert_eq!(scheme.to_string(), "TIERED");
1238 }
1239
1240 #[rstest]
1241 fn tiered_tick_scheme_min_tick_bid() {
1242 let scheme = TieredTickScheme::topix100();
1243 let result = scheme.next_bid_price(0.1, 0, 4).unwrap();
1244 assert_eq!(result, Price::new(0.1, 4));
1245 }
1246
1247 #[rstest]
1248 fn tiered_tick_scheme_min_tick_bid_n1_returns_none() {
1249 let scheme = TieredTickScheme::topix100();
1250 assert!(scheme.next_bid_price(0.1, 1, 4).is_none());
1251 }
1252
1253 #[rstest]
1254 fn tiered_tick_scheme_boundary_tick_equality() {
1255 let scheme = TieredTickScheme::topix100();
1256 let bid = scheme.next_bid_price(1000.0, 0, 4).unwrap();
1257 assert_eq!(bid, Price::new(1000.0, 4));
1258 let ask = scheme.next_ask_price(1000.0, 0, 4).unwrap();
1259 assert_eq!(ask, Price::new(1000.0, 4));
1260 }
1261
1262 #[rstest]
1263 fn tiered_tick_scheme_tier_transition_ask_from_999_9() {
1264 let scheme = TieredTickScheme::topix100();
1265 let ask = scheme.next_ask_price(999.9, 0, 4).unwrap();
1266 assert_eq!(ask, Price::new(999.9, 4));
1267 }
1268
1269 #[rstest]
1270 fn tiered_tick_scheme_tier_transition_bid_from_1000_5() {
1271 let scheme = TieredTickScheme::topix100();
1272 let bid = scheme.next_bid_price(1000.5, 1, 4).unwrap();
1273 assert_eq!(bid, Price::new(1000.0, 4));
1274 }
1275
1276 #[rstest]
1277 fn tiered_tick_scheme_large_n_beyond_bounds_ask() {
1278 let scheme = TieredTickScheme::topix100();
1279 let max = scheme.max_price().as_f64();
1280 assert!(scheme.next_ask_price(max - 1000.0, 100_000, 4).is_none());
1281 }
1282
1283 #[rstest]
1284 fn tiered_tick_scheme_large_n_beyond_bounds_bid() {
1285 let scheme = TieredTickScheme::topix100();
1286 let min = scheme.min_price().as_f64();
1287 assert!(scheme.next_bid_price(min + 1000.0, 100_000, 4).is_none());
1288 }
1289
1290 #[rstest]
1291 fn tiered_tick_scheme_out_of_bounds_ask_far_above() {
1292 let scheme = TieredTickScheme::topix100();
1293 assert!(scheme.next_ask_price(999_999_999.0, 0, 4).is_none());
1294 }
1295
1296 #[rstest]
1297 fn tiered_tick_scheme_idempotent_on_tick() {
1298 let scheme = TieredTickScheme::topix100();
1299 let price = 500.0;
1300 let ask = scheme.next_ask_price(price, 0, 4).unwrap();
1301 let ask2 = scheme.next_ask_price(ask.as_f64(), 0, 4).unwrap();
1302 assert_eq!(ask, ask2);
1303 let bid = scheme.next_bid_price(price, 0, 4).unwrap();
1304 let bid2 = scheme.next_bid_price(bid.as_f64(), 0, 4).unwrap();
1305 assert_eq!(bid, bid2);
1306 }
1307
1308 #[rstest]
1309 fn tiered_tick_scheme_consistency_forward_backward() {
1310 let scheme = TieredTickScheme::topix100();
1311 let start = 5000.0;
1312 let forward = scheme.next_ask_price(start, 10, 4).unwrap();
1313 let back = scheme.next_bid_price(forward.as_f64(), 10, 4).unwrap();
1314 assert!(back.as_f64() <= start);
1315 }
1316
1317 #[rstest]
1318 fn tiered_tick_scheme_cumulative_equals_direct() {
1319 let scheme = TieredTickScheme::topix100();
1320 let price = 1000.0;
1321 let mut cumulative = price;
1322 for _ in 0..5 {
1323 if let Some(result) = scheme.next_ask_price(cumulative, 1, 4) {
1324 cumulative = result.as_f64();
1325 }
1326 }
1327 let direct = scheme.next_ask_price(price, 5, 4).unwrap();
1328 assert!((cumulative - direct.as_f64()).abs() < 1e-10);
1329 }
1330
1331 #[rstest]
1332 #[case(1000.0, 0, 1000.0)]
1333 #[case(1000.25, 0, 1000.5)]
1334 #[case(10_001.0, 0, 10_005.0)]
1335 #[case(10_000_001.0, 0, 10_005_000.0)]
1336 #[case(9999.0, 2, 10_005.0)]
1337 fn tiered_tick_scheme_topix100_ask_parametrized(
1338 #[case] value: f64,
1339 #[case] n: i32,
1340 #[case] expected: f64,
1341 ) {
1342 let scheme = TieredTickScheme::topix100();
1343 let ask = scheme.next_ask_price(value, n, 4).unwrap();
1344 assert_eq!(ask, Price::new(expected, 4));
1345 }
1346
1347 #[rstest]
1348 #[case(1000.75, 0, 1000.5)]
1349 #[case(10_007.0, 0, 10_005.0)]
1350 #[case(10_000_001.0, 0, 10_000_000.0)]
1351 #[case(10_006.0, 2, 9999.0)]
1352 fn tiered_tick_scheme_topix100_bid_parametrized(
1353 #[case] value: f64,
1354 #[case] n: i32,
1355 #[case] expected: f64,
1356 ) {
1357 let scheme = TieredTickScheme::topix100();
1358 let bid = scheme.next_bid_price(value, n, 4).unwrap();
1359 assert_eq!(bid, Price::new(expected, 4));
1360 }
1361
1362 proptest! {
1364 #[rstest]
1365 fn prop_tiered_bid_at_or_below_value(value in 0.1f64..100_000.0) {
1366 let scheme = TieredTickScheme::topix100();
1367 if let Some(bid) = scheme.next_bid_price(value, 0, 4) {
1368 prop_assert!(bid.as_f64() <= value + 1e-9);
1369 }
1370 }
1371 }
1372
1373 proptest! {
1375 #[rstest]
1376 fn prop_tiered_ask_at_or_above_value(value in 0.1f64..100_000.0) {
1377 let scheme = TieredTickScheme::topix100();
1378 if let Some(ask) = scheme.next_ask_price(value, 0, 4) {
1379 prop_assert!(ask.as_f64() >= value - 1e-9);
1380 }
1381 }
1382 }
1383
1384 proptest! {
1386 #[rstest]
1387 fn prop_tiered_bid_less_than_ask_off_grid(value in 0.15f64..99_999.0) {
1388 let scheme = TieredTickScheme::topix100();
1389
1390 if let (Some(bid), Some(ask)) = (
1391 scheme.next_bid_price(value, 0, 4),
1392 scheme.next_ask_price(value, 0, 4),
1393 ) {
1394 prop_assert!(bid <= ask);
1395 }
1396 }
1397 }
1398
1399 proptest! {
1401 #[rstest]
1402 fn prop_tiered_ask_monotonic_in_n(value in 1.0f64..10_000.0) {
1403 let scheme = TieredTickScheme::topix100();
1404 let mut prev: Option<Price> = None;
1405
1406 for n in 0..5 {
1407 if let Some(ask) = scheme.next_ask_price(value, n, 4) {
1408 if let Some(p) = prev {
1409 prop_assert!(ask >= p);
1410 }
1411 prev = Some(ask);
1412 }
1413 }
1414 }
1415 }
1416
1417 proptest! {
1419 #[rstest]
1420 fn prop_tiered_ticks_sorted(
1421 start in 1.0f64..100.0,
1422 step in 0.01f64..10.0,
1423 ) {
1424 let stop = start + step * 10.0;
1425 if let Ok(scheme) = TieredTickScheme::new(&[(start, stop, step)], 2, 100) {
1426 let ticks = scheme.ticks();
1427 for i in 1..ticks.len() {
1428 prop_assert!(ticks[i] > ticks[i - 1]);
1429 }
1430 }
1431 }
1432 }
1433}