1use std::{fmt::Display, str::FromStr, sync::LazyLock};
19
20use nautilus_core::correctness::check_predicate_true;
21
22#[cfg(not(feature = "high-precision"))]
23use crate::types::fixed::f64_to_fixed_i64;
24#[cfg(feature = "high-precision")]
25use crate::types::fixed::f64_to_fixed_i128;
26use crate::types::{
27 Price,
28 fixed::FIXED_SCALAR,
29 price::{PRICE_MAX, PRICE_MIN, PriceRaw},
30};
31
32pub trait TickSchemeRule: Display {
33 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price>;
34 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price>;
35}
36
37pub const BETFAIR_TICK_SCHEME_NAME: &str = "BETFAIR";
38
39const BETFAIR_PRICE_TIERS: [(f64, f64, f64); 10] = [
40 (1.01, 2.0, 0.01),
41 (2.0, 3.0, 0.02),
42 (3.0, 4.0, 0.05),
43 (4.0, 6.0, 0.1),
44 (6.0, 10.0, 0.2),
45 (10.0, 20.0, 0.5),
46 (20.0, 30.0, 1.0),
47 (30.0, 50.0, 2.0),
48 (50.0, 100.0, 5.0),
49 (100.0, 1010.0, 10.0),
50];
51
52pub static BETFAIR_TICK_SCHEME: LazyLock<TieredTickScheme> = LazyLock::new(|| {
53 TieredTickScheme::new(&BETFAIR_PRICE_TIERS, 2, 100)
54 .expect("BETFAIR tick scheme tiers are valid by construction")
55});
56
57#[derive(Clone, Copy, Debug)]
58pub struct FixedTickScheme {
59 tick: f64,
60}
61
62impl PartialEq for FixedTickScheme {
63 fn eq(&self, other: &Self) -> bool {
64 self.tick == other.tick
65 }
66}
67impl Eq for FixedTickScheme {}
68
69impl FixedTickScheme {
70 #[expect(clippy::missing_errors_doc)]
71 pub fn new(tick: f64) -> anyhow::Result<Self> {
72 check_predicate_true(tick > 0.0, "tick must be positive")?;
73 Ok(Self { tick })
74 }
75}
76
77impl TickSchemeRule for FixedTickScheme {
78 #[inline(always)]
79 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
80 let base = (value / self.tick).floor() * self.tick;
81 Some(Price::new(base - f64::from(n) * self.tick, precision))
82 }
83
84 #[inline(always)]
85 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
86 let base = (value / self.tick).ceil() * self.tick;
87 Some(Price::new(base + f64::from(n) * self.tick, precision))
88 }
89}
90
91impl Display for FixedTickScheme {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "FIXED")
94 }
95}
96
97#[derive(Clone, Debug)]
103pub struct TieredTickScheme {
104 ticks: Vec<PriceRaw>,
105 precision: u8,
106}
107
108impl PartialEq for TieredTickScheme {
109 fn eq(&self, other: &Self) -> bool {
110 self.precision == other.precision && self.ticks == other.ticks
111 }
112}
113impl Eq for TieredTickScheme {}
114
115impl TieredTickScheme {
116 pub fn new(
125 tiers: &[(f64, f64, f64)],
126 price_precision: u8,
127 max_ticks_per_tier: usize,
128 ) -> anyhow::Result<Self> {
129 if tiers.is_empty() {
130 anyhow::bail!("tiers must not be empty");
131 }
132
133 for (i, &(start, stop, step)) in tiers.iter().enumerate() {
134 if start.is_nan() || stop.is_nan() || step.is_nan() {
135 anyhow::bail!("tier {i}: values must not be NaN");
136 }
137
138 if start >= stop {
139 anyhow::bail!("tier {i}: start ({start}) must be less than stop ({stop})");
140 }
141
142 if step <= 0.0 {
143 anyhow::bail!("tier {i}: step ({step}) must be positive");
144 }
145
146 if !stop.is_infinite() && step >= (stop - start) {
147 anyhow::bail!(
148 "tier {i}: step ({step}) must be less than range ({} - {} = {})",
149 stop,
150 start,
151 stop - start,
152 );
153 }
154
155 if i > 0 {
156 let prev_stop = tiers[i - 1].1;
157
158 if start < prev_stop {
159 anyhow::bail!(
160 "tier {i}: start ({start}) overlaps previous tier stop ({prev_stop})"
161 );
162 }
163 }
164
165 if !(PRICE_MIN..=PRICE_MAX).contains(&start) {
166 anyhow::bail!("tier {i}: start ({start}) outside Price range");
167 }
168
169 if !stop.is_infinite() && !(PRICE_MIN..=PRICE_MAX).contains(&stop) {
170 anyhow::bail!("tier {i}: stop ({stop}) outside Price range");
171 }
172 }
173
174 let _ = Price::new_checked(0.0, price_precision)?;
175
176 let ticks = Self::build_ticks(tiers, price_precision, max_ticks_per_tier)?;
177
178 if ticks.is_empty() {
179 anyhow::bail!("tier expansion produced no ticks");
180 }
181 Ok(Self {
182 ticks,
183 precision: price_precision,
184 })
185 }
186
187 fn build_ticks(
188 tiers: &[(f64, f64, f64)],
189 precision: u8,
190 max_ticks_per_tier: usize,
191 ) -> anyhow::Result<Vec<PriceRaw>> {
192 let mut all_ticks = Vec::new();
193
194 for &(start, stop, step) in tiers {
195 let effective_stop = if stop.is_infinite() {
196 start + ((max_ticks_per_tier + 1) as f64) * step
197 } else {
198 stop
199 };
200 let mut i = 0;
201 while i < max_ticks_per_tier {
202 let value = start + (i as f64) * step;
203
204 if value >= effective_stop {
205 break;
206 }
207
208 if !value.is_finite() || !(PRICE_MIN..=PRICE_MAX).contains(&value) {
209 anyhow::bail!("expanded tick value {value} outside Price range");
210 }
211 let raw = f64_to_raw(value, precision);
212
213 if all_ticks.last() != Some(&raw) {
214 all_ticks.push(raw);
215 }
216 i += 1;
217 }
218 }
219 Ok(all_ticks)
220 }
221
222 #[inline(always)]
223 fn price_at(&self, index: usize) -> Price {
224 Price {
225 raw: self.ticks[index],
226 precision: self.precision,
227 }
228 }
229
230 #[must_use]
232 pub fn ticks(&self) -> Vec<Price> {
233 self.ticks
234 .iter()
235 .map(|&raw| Price {
236 raw,
237 precision: self.precision,
238 })
239 .collect()
240 }
241
242 #[must_use]
244 pub fn tick_count(&self) -> usize {
245 self.ticks.len()
246 }
247
248 #[must_use]
250 pub fn min_price(&self) -> Price {
251 self.price_at(0)
252 }
253
254 #[must_use]
256 pub fn max_price(&self) -> Price {
257 self.price_at(self.ticks.len() - 1)
258 }
259
260 #[must_use]
262 pub fn precision(&self) -> u8 {
263 self.precision
264 }
265
266 #[must_use]
272 pub fn topix100() -> Self {
273 Self::new(
274 &[
275 (0.1, 1_000.0, 0.1),
276 (1_000.0, 3_000.0, 0.5),
277 (3_000.0, 10_000.0, 1.0),
278 (10_000.0, 30_000.0, 5.0),
279 (30_000.0, 100_000.0, 10.0),
280 (100_000.0, 300_000.0, 50.0),
281 (300_000.0, 1_000_000.0, 100.0),
282 (1_000_000.0, 3_000_000.0, 500.0),
283 (3_000_000.0, 10_000_000.0, 1_000.0),
284 (10_000_000.0, 30_000_000.0, 5_000.0),
285 (30_000_000.0, f64::INFINITY, 10_000.0),
286 ],
287 4,
288 10_000,
289 )
290 .unwrap()
292 }
293
294 #[must_use]
300 pub fn betfair() -> Self {
301 BETFAIR_TICK_SCHEME.clone()
302 }
303}
304
305impl TickSchemeRule for TieredTickScheme {
306 fn next_bid_price(&self, value: f64, n: i32, _precision: u8) -> Option<Price> {
307 if n < 0 {
308 return None;
309 }
310
311 let raw_floor = (value * FIXED_SCALAR).floor() as PriceRaw;
313
314 if raw_floor < self.ticks[0] {
315 return None;
316 }
317
318 let idx = self.ticks.partition_point(|&t| t < raw_floor);
320
321 if idx < self.ticks.len() && self.ticks[idx] == raw_floor {
322 let target = idx as i32 - n;
324
325 if target < 0 {
326 return None;
327 }
328 return Some(self.price_at(target as usize));
329 }
330
331 let effective_idx = idx.min(self.ticks.len());
333 let target = effective_idx as i32 - 1 - n;
334
335 if target < 0 {
336 return None;
337 }
338 Some(self.price_at(target as usize))
339 }
340
341 fn next_ask_price(&self, value: f64, n: i32, _precision: u8) -> Option<Price> {
342 if n < 0 {
343 return None;
344 }
345
346 let raw_ceil = (value * FIXED_SCALAR).ceil() as PriceRaw;
348
349 if raw_ceil > *self.ticks.last()? {
350 return None;
351 }
352
353 let idx = self.ticks.partition_point(|&t| t < raw_ceil);
355 let target = idx as i32 + n;
356
357 if target < 0 || target >= self.ticks.len() as i32 {
358 return None;
359 }
360 Some(self.price_at(target as usize))
361 }
362}
363
364impl Display for TieredTickScheme {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 write!(f, "TIERED")
367 }
368}
369
370#[derive(Clone, Debug, PartialEq, Eq)]
371pub enum TickScheme {
372 Fixed(FixedTickScheme),
373 Tiered(TieredTickScheme),
374 Betfair,
375 Crypto,
376}
377
378impl TickSchemeRule for TickScheme {
379 #[inline(always)]
380 fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
381 match self {
382 Self::Fixed(scheme) => scheme.next_bid_price(value, n, precision),
383 Self::Tiered(scheme) => scheme.next_bid_price(value, n, precision),
384 Self::Betfair => BETFAIR_TICK_SCHEME.next_bid_price(value, n, precision),
385 Self::Crypto => {
386 let increment: f64 = 0.01;
387 let base = (value / increment).floor() * increment;
388 Some(Price::new(base - f64::from(n) * increment, precision))
389 }
390 }
391 }
392
393 #[inline(always)]
394 fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option<Price> {
395 match self {
396 Self::Fixed(scheme) => scheme.next_ask_price(value, n, precision),
397 Self::Tiered(scheme) => scheme.next_ask_price(value, n, precision),
398 Self::Betfair => BETFAIR_TICK_SCHEME.next_ask_price(value, n, precision),
399 Self::Crypto => {
400 let increment: f64 = 0.01;
401 let base = (value / increment).ceil() * increment;
402 Some(Price::new(base + f64::from(n) * increment, precision))
403 }
404 }
405 }
406}
407
408impl Display for TickScheme {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 match self {
411 Self::Fixed(_) => write!(f, "FIXED"),
412 Self::Tiered(scheme) => write!(f, "{scheme}"),
413 Self::Betfair => write!(f, "{BETFAIR_TICK_SCHEME_NAME}"),
414 Self::Crypto => write!(f, "CRYPTO_0_01"),
415 }
416 }
417}
418
419impl FromStr for TickScheme {
420 type Err = anyhow::Error;
421
422 fn from_str(s: &str) -> Result<Self, Self::Err> {
423 match s.trim().to_ascii_uppercase().as_str() {
424 "FIXED" => Ok(Self::Fixed(FixedTickScheme::new(1.0)?)),
425 "TOPIX100" => Ok(Self::Tiered(TieredTickScheme::topix100())),
426 BETFAIR_TICK_SCHEME_NAME => Ok(Self::Betfair),
427 "CRYPTO_0_01" => Ok(Self::Crypto),
428 _ => anyhow::bail!("unknown tick scheme {s}"),
429 }
430 }
431}
432
433#[inline(always)]
435fn f64_to_raw(value: f64, precision: u8) -> PriceRaw {
436 #[cfg(feature = "high-precision")]
437 {
438 f64_to_fixed_i128(value, precision)
439 }
440 #[cfg(not(feature = "high-precision"))]
441 {
442 f64_to_fixed_i64(value, precision)
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use std::str::FromStr;
449
450 use proptest::prelude::*;
451 use rstest::rstest;
452
453 use super::*;
454
455 #[rstest]
456 fn fixed_tick_scheme_prices() {
457 let scheme = FixedTickScheme::new(0.5).unwrap();
458 let bid = scheme.next_bid_price(10.3, 0, 2).unwrap();
459 let ask = scheme.next_ask_price(10.3, 0, 2).unwrap();
460 assert!(bid < ask);
461 }
462
463 #[rstest]
464 #[should_panic(expected = "tick must be positive")]
465 fn fixed_tick_negative() {
466 FixedTickScheme::new(-0.01).unwrap();
467 }
468
469 #[rstest]
470 fn fixed_tick_boundary() {
471 let scheme = FixedTickScheme::new(0.5).unwrap();
472 let price = scheme.next_bid_price(10.5, 0, 2).unwrap();
473 assert_eq!(price, Price::new(10.5, 2));
474 }
475
476 #[rstest]
477 fn fixed_tick_multiple_steps() {
478 let scheme = FixedTickScheme::new(1.0).unwrap();
479 let bid = scheme.next_bid_price(10.0, 2, 1).unwrap();
480 let ask = scheme.next_ask_price(10.0, 3, 1).unwrap();
481 assert_eq!(bid, Price::new(8.0, 1));
482 assert_eq!(ask, Price::new(13.0, 1));
483 }
484
485 #[rstest]
486 fn tick_scheme_round_trip() {
487 let scheme = TickScheme::from_str("CRYPTO_0_01").unwrap();
488 assert_eq!(scheme.to_string(), "CRYPTO_0_01");
489 }
490
491 #[rstest]
492 fn tick_scheme_unknown() {
493 assert!(TickScheme::from_str("UNKNOWN").is_err());
494 }
495
496 #[rstest]
497 fn fixed_tick_zero() {
498 assert!(FixedTickScheme::new(0.0).is_err());
499 }
500
501 #[rstest]
502 fn tiered_tick_scheme_topix100_construction() {
503 let scheme = TieredTickScheme::topix100();
504 assert!(scheme.tick_count() > 0);
505 assert_eq!(scheme.precision(), 4);
506 assert_eq!(scheme.min_price(), Price::new(0.1, 4));
507 }
508
509 #[rstest]
510 fn tiered_tick_scheme_betfair_construction() {
511 let scheme = TieredTickScheme::betfair();
512 assert_eq!(scheme.tick_count(), 350);
513 assert_eq!(scheme.precision(), 2);
514 assert_eq!(scheme.min_price(), Price::new(1.01, 2));
515 assert_eq!(scheme.max_price(), Price::new(1000.0, 2));
516 }
517
518 #[rstest]
519 fn tiered_tick_scheme_ask_at_low_price() {
520 let scheme = TieredTickScheme::topix100();
521 let ask = scheme.next_ask_price(500.0, 0, 4).unwrap();
522 assert_eq!(ask, Price::new(500.0, 4));
523 }
524
525 #[rstest]
526 fn tiered_tick_scheme_bid_at_low_price() {
527 let scheme = TieredTickScheme::topix100();
528 let bid = scheme.next_bid_price(500.0, 0, 4).unwrap();
529 assert_eq!(bid, Price::new(500.0, 4));
530 }
531
532 #[rstest]
533 fn tiered_tick_scheme_ask_steps() {
534 let scheme = TieredTickScheme::topix100();
535 let ask0 = scheme.next_ask_price(500.0, 0, 4).unwrap();
536 let ask1 = scheme.next_ask_price(500.0, 1, 4).unwrap();
537 assert!(ask1 > ask0);
538 assert_eq!(ask1, Price::new(500.1, 4));
539 }
540
541 #[rstest]
542 fn tiered_tick_scheme_bid_steps() {
543 let scheme = TieredTickScheme::topix100();
544 let bid0 = scheme.next_bid_price(500.0, 0, 4).unwrap();
545 let bid1 = scheme.next_bid_price(500.0, 1, 4).unwrap();
546 assert!(bid1 < bid0);
547 assert_eq!(bid1, Price::new(499.9, 4));
548 }
549
550 #[rstest]
551 fn tiered_tick_scheme_tier_boundary_1000() {
552 let scheme = TieredTickScheme::topix100();
553 let ask = scheme.next_ask_price(1000.0, 1, 4).unwrap();
555 assert_eq!(ask, Price::new(1000.5, 4));
556 }
557
558 #[rstest]
559 #[case(3.90, 1, "3.95")]
560 #[case(4.0, 1, "4.10")]
561 fn tiered_tick_scheme_betfair_ask_transition(
562 #[case] value: f64,
563 #[case] n: i32,
564 #[case] expected: &str,
565 ) {
566 let scheme = TieredTickScheme::betfair();
567 let ask = scheme.next_ask_price(value, n, 2).unwrap();
568 assert_eq!(ask, Price::from(expected));
569 }
570
571 #[rstest]
572 #[case(1.499, 0, "1.49")]
573 #[case(2.011, 0, "2.00")]
574 #[case(2.027, 2, "1.99")]
575 fn tiered_tick_scheme_betfair_bid_transition(
576 #[case] value: f64,
577 #[case] n: i32,
578 #[case] expected: &str,
579 ) {
580 let scheme = TieredTickScheme::betfair();
581 let bid = scheme.next_bid_price(value, n, 2).unwrap();
582 assert_eq!(bid, Price::from(expected));
583 }
584
585 #[rstest]
586 fn tiered_tick_scheme_between_ticks() {
587 let scheme = TieredTickScheme::topix100();
588 let ask = scheme.next_ask_price(1000.3, 0, 4).unwrap();
590 assert!(ask.as_f64() >= 1000.3);
591 let bid = scheme.next_bid_price(1000.3, 0, 4).unwrap();
592 assert!(bid.as_f64() <= 1000.3);
593 }
594
595 #[rstest]
596 fn tiered_tick_scheme_off_grid_bid_below_tick() {
597 let scheme = TieredTickScheme::new(&[(1.0, 2.0, 0.05)], 2, 100).unwrap();
599 let bid = scheme.next_bid_price(1.049, 0, 2).unwrap();
600 assert_eq!(bid, Price::new(1.0, 2));
601 }
602
603 #[rstest]
604 fn tiered_tick_scheme_off_grid_ask_above_tick() {
605 let scheme = TieredTickScheme::new(&[(1.0, 2.0, 0.05)], 2, 100).unwrap();
607 let ask = scheme.next_ask_price(1.051, 0, 2).unwrap();
608 assert_eq!(ask, Price::new(1.10, 2));
609 }
610
611 #[rstest]
612 fn tiered_tick_scheme_bid_below_min_returns_none() {
613 let scheme = TieredTickScheme::topix100();
614 assert!(scheme.next_bid_price(0.05, 0, 4).is_none());
615 }
616
617 #[rstest]
618 fn tiered_tick_scheme_ask_beyond_last_tick_returns_none() {
619 let scheme = TieredTickScheme::topix100();
620 let last = scheme.max_price().as_f64();
621 assert!(scheme.next_ask_price(last, 1, 4).is_none());
622 }
623
624 #[rstest]
625 fn tiered_tick_scheme_bid_beyond_last_tick_returns_last() {
626 let scheme = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], 1, 100).unwrap();
627 let bid = scheme.next_bid_price(9.5, 0, 1).unwrap();
629 assert_eq!(bid, Price::new(9.0, 1));
630 }
631
632 #[rstest]
633 fn tiered_tick_scheme_negative_n_returns_none() {
634 let scheme = TieredTickScheme::topix100();
635 assert!(scheme.next_bid_price(500.0, -1, 4).is_none());
636 assert!(scheme.next_ask_price(500.0, -1, 4).is_none());
637 }
638
639 #[rstest]
640 fn tiered_tick_scheme_validation_start_ge_stop() {
641 let result = TieredTickScheme::new(&[(100.0, 50.0, 1.0)], 2, 100);
642 assert!(result.is_err());
643 }
644
645 #[rstest]
646 fn tiered_tick_scheme_validation_negative_step() {
647 let result = TieredTickScheme::new(&[(0.0, 100.0, -1.0)], 2, 100);
648 assert!(result.is_err());
649 }
650
651 #[rstest]
652 fn tiered_tick_scheme_validation_step_ge_range() {
653 let result = TieredTickScheme::new(&[(0.0, 100.0, 200.0)], 2, 100);
654 assert!(result.is_err());
655 }
656
657 #[rstest]
658 fn tiered_tick_scheme_validation_invalid_precision() {
659 let result = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], 50, 100);
660 assert!(result.is_err());
661 }
662
663 #[rstest]
664 fn tiered_tick_scheme_validation_empty_tiers() {
665 let result = TieredTickScheme::new(&[], 2, 100);
666 assert!(result.is_err());
667 }
668
669 #[rstest]
670 fn tiered_tick_scheme_validation_non_monotonic_tiers() {
671 let result = TieredTickScheme::new(&[(10.0, 20.0, 1.0), (1.0, 10.0, 1.0)], 1, 100);
672 assert!(result.is_err());
673 }
674
675 #[rstest]
676 fn tiered_tick_scheme_validation_overlapping_tiers() {
677 let result = TieredTickScheme::new(&[(1.0, 10.0, 1.0), (5.0, 15.0, 1.0)], 1, 100);
678 assert!(result.is_err());
679 }
680
681 #[rstest]
682 fn tiered_tick_scheme_finite_tier_includes_all_ticks() {
683 let scheme = TieredTickScheme::new(&[(0.0, 0.3, 0.1)], 1, 100).unwrap();
685 assert_eq!(scheme.tick_count(), 3);
686 }
687
688 #[rstest]
689 fn tiered_tick_scheme_simple_two_tiers() {
690 let scheme =
691 TieredTickScheme::new(&[(1.0, 10.0, 1.0), (10.0, 100.0, 5.0)], 2, 100).unwrap();
692 let ticks = scheme.ticks();
693 assert_eq!(ticks[0], Price::new(1.0, 2));
696 assert_eq!(ticks[8], Price::new(9.0, 2));
697 assert_eq!(ticks[9], Price::new(10.0, 2));
698 assert_eq!(ticks[10], Price::new(15.0, 2));
699 }
700
701 #[rstest]
702 fn tiered_tick_scheme_infinity_tier() {
703 let scheme = TieredTickScheme::new(&[(100.0, f64::INFINITY, 10.0)], 1, 5).unwrap();
704 assert_eq!(scheme.tick_count(), 5);
705 let ticks = scheme.ticks();
706 assert_eq!(ticks[0], Price::new(100.0, 1));
707 assert_eq!(ticks[4], Price::new(140.0, 1));
708 }
709
710 #[rstest]
711 fn tiered_tick_scheme_from_str_topix100() {
712 let scheme = TickScheme::from_str("TOPIX100").unwrap();
713 assert_eq!(scheme.to_string(), "TIERED");
714 }
715
716 #[rstest]
717 fn tiered_tick_scheme_from_str_betfair() {
718 let scheme = TickScheme::from_str("BETFAIR").unwrap();
719 assert_eq!(scheme.to_string(), BETFAIR_TICK_SCHEME_NAME);
720 assert_eq!(
721 scheme.next_ask_price(4.0, 1, 2).unwrap(),
722 Price::new(4.1, 2)
723 );
724 }
725
726 #[rstest]
727 fn tiered_tick_scheme_display() {
728 let scheme = TieredTickScheme::new(&[(1.0, 10.0, 1.0)], 2, 100).unwrap();
729 assert_eq!(scheme.to_string(), "TIERED");
730 }
731
732 #[rstest]
733 fn tiered_tick_scheme_validation_start_equals_stop() {
734 let result = TieredTickScheme::new(&[(2.0, 2.0, 0.1)], 2, 100);
735 assert!(result.is_err());
736 }
737
738 #[rstest]
739 fn tiered_tick_scheme_validation_nan_stop() {
740 let result = TieredTickScheme::new(&[(1.0, f64::NAN, 1.0)], 2, 100);
741 assert!(result.is_err());
742 }
743
744 #[rstest]
745 fn tiered_tick_scheme_validation_nan_start() {
746 let result = TieredTickScheme::new(&[(f64::NAN, 10.0, 1.0)], 2, 100);
747 assert!(result.is_err());
748 }
749
750 #[rstest]
751 fn tiered_tick_scheme_validation_nan_step() {
752 let result = TieredTickScheme::new(&[(1.0, 10.0, f64::NAN)], 2, 100);
753 assert!(result.is_err());
754 }
755
756 #[rstest]
757 fn tiered_tick_scheme_validation_zero_step() {
758 let result = TieredTickScheme::new(&[(1.0, 2.0, 0.0)], 2, 100);
759 assert!(result.is_err());
760 }
761
762 #[rstest]
763 fn tiered_tick_scheme_min_tick_bid() {
764 let scheme = TieredTickScheme::topix100();
765 let result = scheme.next_bid_price(0.1, 0, 4).unwrap();
766 assert_eq!(result, Price::new(0.1, 4));
767 }
768
769 #[rstest]
770 fn tiered_tick_scheme_min_tick_bid_n1_returns_none() {
771 let scheme = TieredTickScheme::topix100();
772 assert!(scheme.next_bid_price(0.1, 1, 4).is_none());
773 }
774
775 #[rstest]
776 fn tiered_tick_scheme_boundary_tick_equality() {
777 let scheme = TieredTickScheme::topix100();
778 let bid = scheme.next_bid_price(1000.0, 0, 4).unwrap();
779 assert_eq!(bid, Price::new(1000.0, 4));
780 let ask = scheme.next_ask_price(1000.0, 0, 4).unwrap();
781 assert_eq!(ask, Price::new(1000.0, 4));
782 }
783
784 #[rstest]
785 fn tiered_tick_scheme_tier_transition_ask_from_999_9() {
786 let scheme = TieredTickScheme::topix100();
787 let ask = scheme.next_ask_price(999.9, 0, 4).unwrap();
788 assert_eq!(ask, Price::new(999.9, 4));
789 }
790
791 #[rstest]
792 fn tiered_tick_scheme_tier_transition_bid_from_1000_5() {
793 let scheme = TieredTickScheme::topix100();
794 let bid = scheme.next_bid_price(1000.5, 1, 4).unwrap();
795 assert_eq!(bid, Price::new(1000.0, 4));
796 }
797
798 #[rstest]
799 fn tiered_tick_scheme_large_n_beyond_bounds_ask() {
800 let scheme = TieredTickScheme::topix100();
801 let max = scheme.max_price().as_f64();
802 assert!(scheme.next_ask_price(max - 1000.0, 100_000, 4).is_none());
803 }
804
805 #[rstest]
806 fn tiered_tick_scheme_large_n_beyond_bounds_bid() {
807 let scheme = TieredTickScheme::topix100();
808 let min = scheme.min_price().as_f64();
809 assert!(scheme.next_bid_price(min + 1000.0, 100_000, 4).is_none());
810 }
811
812 #[rstest]
813 fn tiered_tick_scheme_out_of_bounds_ask_far_above() {
814 let scheme = TieredTickScheme::topix100();
815 assert!(scheme.next_ask_price(999_999_999.0, 0, 4).is_none());
816 }
817
818 #[rstest]
819 fn tiered_tick_scheme_idempotent_on_tick() {
820 let scheme = TieredTickScheme::topix100();
821 let price = 500.0;
822 let ask = scheme.next_ask_price(price, 0, 4).unwrap();
823 let ask2 = scheme.next_ask_price(ask.as_f64(), 0, 4).unwrap();
824 assert_eq!(ask, ask2);
825 let bid = scheme.next_bid_price(price, 0, 4).unwrap();
826 let bid2 = scheme.next_bid_price(bid.as_f64(), 0, 4).unwrap();
827 assert_eq!(bid, bid2);
828 }
829
830 #[rstest]
831 fn tiered_tick_scheme_consistency_forward_backward() {
832 let scheme = TieredTickScheme::topix100();
833 let start = 5000.0;
834 let forward = scheme.next_ask_price(start, 10, 4).unwrap();
835 let back = scheme.next_bid_price(forward.as_f64(), 10, 4).unwrap();
836 assert!(back.as_f64() <= start);
837 }
838
839 #[rstest]
840 fn tiered_tick_scheme_cumulative_equals_direct() {
841 let scheme = TieredTickScheme::topix100();
842 let price = 1000.0;
843 let mut cumulative = price;
844 for _ in 0..5 {
845 if let Some(result) = scheme.next_ask_price(cumulative, 1, 4) {
846 cumulative = result.as_f64();
847 }
848 }
849 let direct = scheme.next_ask_price(price, 5, 4).unwrap();
850 assert!((cumulative - direct.as_f64()).abs() < 1e-10);
851 }
852
853 #[rstest]
854 #[case(1000.0, 0, 1000.0)]
855 #[case(1000.25, 0, 1000.5)]
856 #[case(10_001.0, 0, 10_005.0)]
857 #[case(10_000_001.0, 0, 10_005_000.0)]
858 #[case(9999.0, 2, 10_005.0)]
859 fn tiered_tick_scheme_topix100_ask_parametrized(
860 #[case] value: f64,
861 #[case] n: i32,
862 #[case] expected: f64,
863 ) {
864 let scheme = TieredTickScheme::topix100();
865 let ask = scheme.next_ask_price(value, n, 4).unwrap();
866 assert_eq!(ask, Price::new(expected, 4));
867 }
868
869 #[rstest]
870 #[case(1000.75, 0, 1000.5)]
871 #[case(10_007.0, 0, 10_005.0)]
872 #[case(10_000_001.0, 0, 10_000_000.0)]
873 #[case(10_006.0, 2, 9999.0)]
874 fn tiered_tick_scheme_topix100_bid_parametrized(
875 #[case] value: f64,
876 #[case] n: i32,
877 #[case] expected: f64,
878 ) {
879 let scheme = TieredTickScheme::topix100();
880 let bid = scheme.next_bid_price(value, n, 4).unwrap();
881 assert_eq!(bid, Price::new(expected, 4));
882 }
883
884 proptest! {
886 #[rstest]
887 fn prop_tiered_bid_at_or_below_value(value in 0.1f64..100_000.0) {
888 let scheme = TieredTickScheme::topix100();
889 if let Some(bid) = scheme.next_bid_price(value, 0, 4) {
890 prop_assert!(bid.as_f64() <= value + 1e-9);
891 }
892 }
893 }
894
895 proptest! {
897 #[rstest]
898 fn prop_tiered_ask_at_or_above_value(value in 0.1f64..100_000.0) {
899 let scheme = TieredTickScheme::topix100();
900 if let Some(ask) = scheme.next_ask_price(value, 0, 4) {
901 prop_assert!(ask.as_f64() >= value - 1e-9);
902 }
903 }
904 }
905
906 proptest! {
908 #[rstest]
909 fn prop_tiered_bid_less_than_ask_off_grid(value in 0.15f64..99_999.0) {
910 let scheme = TieredTickScheme::topix100();
911
912 if let (Some(bid), Some(ask)) = (
913 scheme.next_bid_price(value, 0, 4),
914 scheme.next_ask_price(value, 0, 4),
915 ) {
916 prop_assert!(bid <= ask);
917 }
918 }
919 }
920
921 proptest! {
923 #[rstest]
924 fn prop_tiered_ask_monotonic_in_n(value in 1.0f64..10_000.0) {
925 let scheme = TieredTickScheme::topix100();
926 let mut prev: Option<Price> = None;
927
928 for n in 0..5 {
929 if let Some(ask) = scheme.next_ask_price(value, n, 4) {
930 if let Some(p) = prev {
931 prop_assert!(ask >= p);
932 }
933 prev = Some(ask);
934 }
935 }
936 }
937 }
938
939 proptest! {
941 #[rstest]
942 fn prop_tiered_ticks_sorted(
943 start in 1.0f64..100.0,
944 step in 0.01f64..10.0,
945 ) {
946 let stop = start + step * 10.0;
947 if let Ok(scheme) = TieredTickScheme::new(&[(start, stop, step)], 2, 100) {
948 let ticks = scheme.ticks();
949 for i in 1..ticks.len() {
950 prop_assert!(ticks[i] > ticks[i - 1]);
951 }
952 }
953 }
954 }
955}