1use crate::error::FinError;
19use chrono::{DateTime, TimeZone, Timelike, Utc};
20use rust_decimal::Decimal;
21use std::sync::Arc;
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
34#[serde(transparent)]
35pub struct Symbol(Arc<str>);
36
37impl Symbol {
38 pub fn new(s: impl AsRef<str>) -> Result<Self, FinError> {
43 let s = s.as_ref();
44 if s.is_empty() || s.chars().any(char::is_whitespace) {
45 return Err(FinError::InvalidSymbol(s.to_owned()));
46 }
47 Ok(Self(Arc::from(s)))
48 }
49
50 pub fn as_str(&self) -> &str {
52 &self.0
53 }
54
55 pub fn len(&self) -> usize {
57 self.0.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
65 self.0.is_empty()
66 }
67}
68
69impl std::fmt::Display for Symbol {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(&self.0)
72 }
73}
74
75impl AsRef<str> for Symbol {
76 fn as_ref(&self) -> &str {
77 &self.0
78 }
79}
80
81impl std::borrow::Borrow<str> for Symbol {
82 fn borrow(&self) -> &str {
83 &self.0
84 }
85}
86
87impl TryFrom<String> for Symbol {
88 type Error = FinError;
89
90 fn try_from(s: String) -> Result<Self, Self::Error> {
91 Symbol::new(s)
92 }
93}
94
95impl TryFrom<&str> for Symbol {
96 type Error = FinError;
97
98 fn try_from(s: &str) -> Result<Self, Self::Error> {
99 Symbol::new(s)
100 }
101}
102
103impl PartialOrd for Symbol {
104 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
105 Some(self.cmp(other))
106 }
107}
108
109impl Ord for Symbol {
110 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
111 self.0.as_ref().cmp(other.0.as_ref())
112 }
113}
114
115impl From<Symbol> for String {
116 fn from(s: Symbol) -> Self {
117 s.as_str().to_owned()
118 }
119}
120
121impl From<Symbol> for Arc<str> {
122 fn from(s: Symbol) -> Self {
123 s.0.clone()
124 }
125}
126
127#[derive(
137 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
138)]
139pub struct Price(Decimal);
140
141impl Price {
142 pub fn new(d: Decimal) -> Result<Self, FinError> {
147 if d <= Decimal::ZERO {
148 return Err(FinError::InvalidPrice(d));
149 }
150 Ok(Self(d))
151 }
152
153 pub fn value(&self) -> Decimal {
155 self.0
156 }
157
158 pub fn to_f64(&self) -> f64 {
160 rust_decimal::prelude::ToPrimitive::to_f64(&self.0).unwrap_or(f64::NAN)
161 }
162
163 pub fn from_f64(f: f64) -> Option<Self> {
165 use rust_decimal::prelude::FromPrimitive;
166 let d = Decimal::from_f64(f)?;
167 Self::new(d).ok()
168 }
169
170 pub fn to_string_with_dp(&self, dp: u32) -> String {
174 self.0.round_dp(dp).to_string()
175 }
176}
177
178impl Price {
179 pub fn pct_change_to(self, other: Price) -> Decimal {
183 (other.0 - self.0) / self.0 * Decimal::ONE_HUNDRED
184 }
185
186 pub fn mid(self, other: Price) -> Price {
188 Price((self.0 + other.0) / Decimal::TWO)
189 }
190}
191
192impl Price {
193 pub fn abs_diff(self, other: Price) -> Decimal {
195 (self.0 - other.0).abs()
196 }
197
198 pub fn snap_to_tick(self, tick_size: Decimal) -> Option<Price> {
213 if tick_size <= Decimal::ZERO {
214 return None;
215 }
216 let rounded = (self.0 / tick_size).round() * tick_size;
217 Price::new(rounded).ok()
218 }
219
220 pub fn clamp(self, lo: Price, hi: Price) -> Price {
224 if self.0 < lo.0 {
225 lo
226 } else if self.0 > hi.0 {
227 hi
228 } else {
229 self
230 }
231 }
232}
233
234impl Price {
235 pub fn round_to(self, dp: u32) -> Option<Price> {
239 let rounded = self.0.round_dp(dp);
240 Price::new(rounded).ok()
241 }
242
243 pub fn round_half_up(self, dp: u32) -> Option<Price> {
248 use rust_decimal::RoundingStrategy;
249 let rounded = self.0.round_dp_with_strategy(dp, RoundingStrategy::MidpointAwayFromZero);
250 Price::new(rounded).ok()
251 }
252}
253
254impl Price {
255 pub fn checked_add(self, other: Price) -> Option<Price> {
259 let sum = self.0.checked_add(other.0)?;
260 Price::new(sum).ok()
261 }
262}
263
264impl Price {
265 pub fn checked_mul(self, qty: Quantity) -> Option<Decimal> {
270 self.0.checked_mul(qty.0)
271 }
272}
273
274impl Price {
275 pub fn midpoint(bid: Price, ask: Price) -> Decimal {
279 (bid.0 + ask.0) / Decimal::TWO
280 }
281
282 pub fn pct_move(self, pct: Decimal) -> Option<Price> {
287 let result = self.0 * (Decimal::ONE + pct / Decimal::ONE_HUNDRED);
288 Price::new(result).ok()
289 }
290
291 pub fn lerp(self, other: Price, t: Decimal) -> Option<Price> {
296 if t < Decimal::ZERO || t > Decimal::ONE {
297 return None;
298 }
299 let result = self.0 + (other.0 - self.0) * t;
300 Price::new(result).ok()
301 }
302
303 pub fn is_within_pct(self, other: Price, pct: Decimal) -> bool {
308 if pct < Decimal::ZERO {
309 return false;
310 }
311 let diff = (self.0 - other.0).abs();
312 diff / self.0 * Decimal::ONE_HUNDRED <= pct
313 }
314
315 pub fn distance_pct(self, other: Price) -> Decimal {
319 (other.0 - self.0) / self.0 * Decimal::ONE_HUNDRED
320 }
321
322 pub fn round_to_tick(self, tick_size: Decimal) -> Option<Price> {
328 self.snap_to_tick(tick_size)
329 }
330}
331
332impl std::fmt::Display for Price {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "{}", self.0)
335 }
336}
337
338impl std::ops::Add<Price> for Price {
340 type Output = Decimal;
341 fn add(self, rhs: Price) -> Decimal {
342 self.0 + rhs.0
343 }
344}
345
346impl std::ops::Sub<Price> for Price {
348 type Output = Decimal;
349 fn sub(self, rhs: Price) -> Decimal {
350 self.0 - rhs.0
351 }
352}
353
354impl std::ops::Mul<Quantity> for Price {
356 type Output = Decimal;
357 fn mul(self, rhs: Quantity) -> Decimal {
358 self.0 * rhs.0
359 }
360}
361
362impl std::ops::Mul<Decimal> for Price {
364 type Output = Option<Price>;
365 fn mul(self, rhs: Decimal) -> Option<Price> {
366 Price::new(self.0 * rhs).ok()
367 }
368}
369
370#[derive(
380 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
381)]
382pub struct Quantity(Decimal);
383
384impl Quantity {
385 pub fn new(d: Decimal) -> Result<Self, FinError> {
390 if d < Decimal::ZERO {
391 return Err(FinError::InvalidQuantity(d));
392 }
393 Ok(Self(d))
394 }
395
396 pub fn zero() -> Self {
398 Self(Decimal::ZERO)
399 }
400
401 pub fn is_zero(&self) -> bool {
403 self.0 == Decimal::ZERO
404 }
405
406 pub fn value(&self) -> Decimal {
408 self.0
409 }
410
411 pub fn to_f64(&self) -> f64 {
413 rust_decimal::prelude::ToPrimitive::to_f64(&self.0).unwrap_or(f64::NAN)
414 }
415
416 pub fn from_f64(f: f64) -> Option<Self> {
418 use rust_decimal::prelude::FromPrimitive;
419 let d = Decimal::from_f64(f)?;
420 Self::new(d).ok()
421 }
422}
423
424impl Quantity {
425 pub fn checked_add(self, other: Quantity) -> Option<Quantity> {
427 self.0.checked_add(other.0).map(Quantity)
428 }
429
430 pub fn checked_sub(self, other: Quantity) -> Option<Quantity> {
432 let result = self.0.checked_sub(other.0)?;
433 if result < Decimal::ZERO {
434 None
435 } else {
436 Some(Quantity(result))
437 }
438 }
439
440 pub fn abs(self) -> Quantity {
446 Quantity(self.0.abs())
447 }
448
449 pub fn split(self, n: usize) -> Vec<Quantity> {
454 if n == 0 {
455 return Vec::new();
456 }
457 let part = self.0 / Decimal::from(n as u64);
458 let mut parts: Vec<Quantity> = (0..n - 1).map(|_| Quantity(part)).collect();
459 let assigned: Decimal = part * Decimal::from((n - 1) as u64);
460 parts.push(Quantity(self.0 - assigned));
461 parts
462 }
463
464 pub fn proportion_of(self, total: Quantity) -> Option<Decimal> {
468 if total.is_zero() {
469 return None;
470 }
471 Some(self.0 / total.0)
472 }
473
474 pub fn scale(self, factor: Decimal) -> Option<Quantity> {
479 if factor < Decimal::ZERO {
480 return None;
481 }
482 Some(Quantity(self.0 * factor))
483 }
484}
485
486impl std::fmt::Display for Quantity {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 write!(f, "{}", self.0)
489 }
490}
491
492impl std::ops::Add<Quantity> for Quantity {
494 type Output = Quantity;
495 fn add(self, rhs: Quantity) -> Quantity {
496 Quantity(self.0 + rhs.0)
497 }
498}
499
500impl std::ops::Sub<Quantity> for Quantity {
502 type Output = Decimal;
503 fn sub(self, rhs: Quantity) -> Decimal {
504 self.0 - rhs.0
505 }
506}
507
508impl std::ops::Mul<Decimal> for Quantity {
510 type Output = Decimal;
511 fn mul(self, rhs: Decimal) -> Decimal {
512 self.0 * rhs
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
518pub enum Side {
519 Bid,
521 Ask,
523}
524
525impl Side {
526 pub fn opposite(self) -> Side {
528 match self {
529 Side::Bid => Side::Ask,
530 Side::Ask => Side::Bid,
531 }
532 }
533}
534
535impl std::fmt::Display for Side {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 match self {
538 Side::Bid => f.write_str("Bid"),
539 Side::Ask => f.write_str("Ask"),
540 }
541 }
542}
543
544#[derive(
549 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
550)]
551pub struct NanoTimestamp(i64);
552
553impl NanoTimestamp {
554 pub const MIN: NanoTimestamp = NanoTimestamp(i64::MIN);
556
557 pub const MAX: NanoTimestamp = NanoTimestamp(i64::MAX);
559 pub fn new(nanos: i64) -> Self {
561 Self(nanos)
562 }
563
564 pub fn nanos(&self) -> i64 {
566 self.0
567 }
568
569 pub fn as_nanos(&self) -> u128 {
573 self.0.max(0) as u128
574 }
575
576 pub fn now() -> Self {
580 Self(Utc::now().timestamp_nanos_opt().unwrap_or(0))
581 }
582
583 pub fn elapsed(&self) -> i64 {
587 NanoTimestamp::now().0 - self.0
588 }
589
590 pub fn duration_since(&self, other: NanoTimestamp) -> i64 {
594 self.0 - other.0
595 }
596
597 pub fn diff_millis(&self, other: NanoTimestamp) -> i64 {
602 (self.0 - other.0) / 1_000_000
603 }
604
605 pub fn elapsed_nanos_since(&self, other: NanoTimestamp) -> Option<i64> {
610 let diff = self.0 - other.0;
611 if diff >= 0 {
612 Some(diff)
613 } else {
614 None
615 }
616 }
617
618 pub fn add_nanos(&self, nanos: i64) -> NanoTimestamp {
620 NanoTimestamp(self.0 + nanos)
621 }
622
623 pub fn add_millis(&self, ms: i64) -> NanoTimestamp {
625 NanoTimestamp(self.0 + ms * 1_000_000)
626 }
627
628 pub fn add_seconds(&self, secs: i64) -> NanoTimestamp {
630 NanoTimestamp(self.0 + secs * 1_000_000_000)
631 }
632
633 pub fn add_minutes(&self, minutes: i64) -> NanoTimestamp {
635 NanoTimestamp(self.0 + minutes * 60_000_000_000)
636 }
637
638 pub fn add_hours(&self, hours: i64) -> NanoTimestamp {
640 NanoTimestamp(self.0 + hours * 3_600_000_000_000)
641 }
642
643 pub fn is_before(&self, other: NanoTimestamp) -> bool {
645 self.0 < other.0
646 }
647
648 pub fn is_after(&self, other: NanoTimestamp) -> bool {
650 self.0 > other.0
651 }
652
653 pub fn is_same_second(&self, other: NanoTimestamp) -> bool {
658 self.0.div_euclid(1_000_000_000) == other.0.div_euclid(1_000_000_000)
659 }
660
661 pub fn is_same_minute(&self, other: NanoTimestamp) -> bool {
666 self.0.div_euclid(60_000_000_000) == other.0.div_euclid(60_000_000_000)
667 }
668
669 pub fn from_millis(ms: i64) -> Self {
671 Self(ms * 1_000_000)
672 }
673
674 pub fn to_millis(&self) -> i64 {
676 self.0 / 1_000_000
677 }
678
679 pub fn from_secs(secs: i64) -> Self {
681 Self(secs * 1_000_000_000)
682 }
683
684 pub fn to_secs(&self) -> i64 {
686 self.0 / 1_000_000_000
687 }
688
689 pub fn from_datetime(dt: DateTime<Utc>) -> Self {
693 Self(dt.timestamp_nanos_opt().unwrap_or(0))
694 }
695
696 pub fn to_datetime(&self) -> DateTime<Utc> {
698 let secs = self.0 / 1_000_000_000;
699 #[allow(clippy::cast_sign_loss)]
700 let nanos = (self.0 % 1_000_000_000) as u32;
701 Utc.timestamp_opt(secs, nanos).single().unwrap_or_else(|| {
702 Utc.timestamp_opt(0, 0)
703 .single()
704 .unwrap_or(DateTime::<Utc>::MIN_UTC)
705 })
706 }
707
708 pub fn to_seconds(&self) -> f64 {
710 self.0 as f64 / 1_000_000_000.0
711 }
712
713 pub fn duration_millis(self, other: NanoTimestamp) -> i64 {
717 (self.0 - other.0) / 1_000_000
718 }
719
720 pub fn min(self, other: NanoTimestamp) -> NanoTimestamp {
722 if self.0 <= other.0 { self } else { other }
723 }
724
725 pub fn max(self, other: NanoTimestamp) -> NanoTimestamp {
727 if self.0 >= other.0 { self } else { other }
728 }
729
730 pub fn elapsed_since(self, earlier: NanoTimestamp) -> i64 {
735 self.0 - earlier.0
736 }
737
738 pub fn seconds_since(self, earlier: NanoTimestamp) -> i64 {
742 (self.0 - earlier.0) / 1_000_000_000
743 }
744
745 pub fn minutes_since(self, earlier: NanoTimestamp) -> i64 {
749 (self.0 - earlier.0) / 60_000_000_000
750 }
751
752 pub fn hours_since(self, earlier: NanoTimestamp) -> i64 {
756 (self.0 - earlier.0) / 3_600_000_000_000
757 }
758
759 pub fn round_down_to(&self, period_nanos: i64) -> NanoTimestamp {
766 if period_nanos == 0 {
767 return *self;
768 }
769 NanoTimestamp(self.0 - self.0.rem_euclid(period_nanos))
770 }
771
772 pub fn to_date_string(&self) -> String {
776 use chrono::{DateTime, Utc};
777 let secs = self.0 / 1_000_000_000;
778 let nanos_part = (self.0 % 1_000_000_000).unsigned_abs() as u32;
779 let dt = DateTime::<Utc>::from_timestamp(secs, nanos_part)
780 .unwrap_or_default();
781 dt.format("%Y-%m-%d").to_string()
782 }
783
784 pub fn is_same_day(&self, other: NanoTimestamp) -> bool {
788 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
790 self.0.div_euclid(DAY_NANOS) == other.0.div_euclid(DAY_NANOS)
791 }
792
793 pub fn floor_to_hour(&self) -> NanoTimestamp {
799 const HOUR_NANOS: i64 = 3_600 * 1_000_000_000;
800 NanoTimestamp(self.0.div_euclid(HOUR_NANOS) * HOUR_NANOS)
801 }
802
803 pub fn hour_of_day(self) -> u8 {
805 use chrono::Timelike;
806 self.to_datetime().hour() as u8
807 }
808
809 pub fn minute_of_hour(self) -> u8 {
811 use chrono::Timelike;
812 self.to_datetime().minute() as u8
813 }
814
815 pub fn is_market_hours(self, open_hour: u8, close_hour: u8) -> bool {
821 if open_hour >= close_hour { return false; }
822 let h = self.hour_of_day();
823 h >= open_hour && h < close_hour
824 }
825
826 pub fn floor_to_day(&self) -> NanoTimestamp {
830 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
831 NanoTimestamp(self.0.div_euclid(DAY_NANOS) * DAY_NANOS)
832 }
833
834 pub fn floor_to_minute(&self) -> NanoTimestamp {
836 const MINUTE_NANOS: i64 = 60 * 1_000_000_000;
837 NanoTimestamp(self.0.div_euclid(MINUTE_NANOS) * MINUTE_NANOS)
838 }
839
840 pub fn elapsed_seconds(&self, other: NanoTimestamp) -> f64 {
844 (self.0 - other.0) as f64 / 1_000_000_000.0
845 }
846
847 pub fn to_datetime_string(&self) -> String {
851 use chrono::{DateTime, Utc};
852 let secs = self.0 / 1_000_000_000;
853 let nanos_part = (self.0 % 1_000_000_000).unsigned_abs() as u32;
854 let dt = DateTime::<Utc>::from_timestamp(secs, nanos_part)
855 .unwrap_or_default();
856 dt.format("%Y-%m-%d %H:%M:%S").to_string()
857 }
858
859 pub fn is_between(self, start: NanoTimestamp, end: NanoTimestamp) -> bool {
861 self.0 >= start.0 && self.0 <= end.0
862 }
863
864 pub fn to_unix_ms(self) -> i64 {
866 self.0 / 1_000_000
867 }
868
869 pub fn to_unix_seconds(self) -> i64 {
871 self.0 / 1_000_000_000
872 }
873
874 pub fn second_of_minute(self) -> u8 {
876 use chrono::Timelike;
877 self.to_datetime().second() as u8
878 }
879
880 pub fn day_of_week(self) -> u8 {
884 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
885 let days = self.0.div_euclid(DAY_NANOS);
886 ((days + 3).rem_euclid(7)) as u8
888 }
889
890 pub fn sub_minutes(&self, minutes: i64) -> NanoTimestamp {
894 NanoTimestamp(self.0 - minutes * 60_000_000_000)
895 }
896
897 pub fn is_weekend(self) -> bool {
899 let dow = self.day_of_week();
900 dow == 5 || dow == 6
901 }
902
903 pub fn start_of_week(self) -> NanoTimestamp {
905 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
906 let dow = self.day_of_week() as i64; NanoTimestamp(self.floor_to_day().0 - dow * DAY_NANOS)
908 }
909
910 pub fn add_days(&self, days: i64) -> NanoTimestamp {
912 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
913 NanoTimestamp(self.0 + days * DAY_NANOS)
914 }
915
916 pub fn minutes_between(self, other: NanoTimestamp) -> u64 {
918 const MINUTE_NANOS: u64 = 60 * 1_000_000_000;
919 (self.0 - other.0).unsigned_abs() / MINUTE_NANOS
920 }
921
922 pub fn seconds_between(self, other: NanoTimestamp) -> u64 {
924 const SECOND_NANOS: u64 = 1_000_000_000;
925 (self.0 - other.0).unsigned_abs() / SECOND_NANOS
926 }
927
928 pub fn day_of_year(self) -> u16 {
932 use chrono::Datelike;
933 self.to_datetime().ordinal() as u16
934 }
935
936 pub fn quarter(self) -> u8 {
940 use chrono::Datelike;
941 let month = self.to_datetime().month();
942 ((month - 1) / 3 + 1) as u8
943 }
944
945 pub fn week_of_year(self) -> u32 {
949 use chrono::Datelike;
950 self.to_datetime().iso_week().week()
951 }
952
953 pub fn is_same_week(self, other: NanoTimestamp) -> bool {
955 use chrono::Datelike;
956 let a = self.to_datetime().iso_week();
957 let b = other.to_datetime().iso_week();
958 a.week() == b.week() && a.year() == b.year()
959 }
960
961 pub fn is_same_month(self, other: NanoTimestamp) -> bool {
963 use chrono::Datelike;
964 let a = self.to_datetime();
965 let b = other.to_datetime();
966 a.year() == b.year() && a.month() == b.month()
967 }
968
969 pub fn floor_to_week(self) -> NanoTimestamp {
971 use chrono::{Datelike, Duration, TimeZone};
972 let dt = self.to_datetime();
973 let days_since_monday = dt.weekday().num_days_from_monday() as i64;
974 let monday = dt - Duration::days(days_since_monday);
975 let monday_midnight = Utc
976 .with_ymd_and_hms(monday.year(), monday.month(), monday.day(), 0, 0, 0)
977 .single()
978 .expect("valid date");
979 NanoTimestamp::from_datetime(monday_midnight)
980 }
981
982 pub fn is_same_year(self, other: NanoTimestamp) -> bool {
984 use chrono::Datelike;
985 self.to_datetime().year() == other.to_datetime().year()
986 }
987
988 pub fn days_between(self, other: NanoTimestamp) -> u64 {
990 let diff_nanos = (self.0 - other.0).unsigned_abs();
991 diff_nanos / 86_400_000_000_000
992 }
993
994 pub fn end_of_day(self) -> NanoTimestamp {
996 use chrono::{Datelike, TimeZone, Timelike};
997 let dt = chrono::Utc.timestamp_nanos(self.0);
998 let eod = chrono::Utc
999 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 23, 59, 59)
1000 .single()
1001 .map(|d| d.with_nanosecond(999_999_999).unwrap_or(d))
1002 .unwrap_or(dt);
1003 NanoTimestamp(eod.timestamp_nanos_opt().unwrap_or(self.0))
1004 }
1005
1006 pub fn start_of_month(self) -> NanoTimestamp {
1008 use chrono::{Datelike, TimeZone};
1009 let dt = chrono::Utc.timestamp_nanos(self.0);
1010 let som = chrono::Utc
1011 .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
1012 .single()
1013 .unwrap_or(dt);
1014 NanoTimestamp(som.timestamp_nanos_opt().unwrap_or(self.0))
1015 }
1016
1017 pub fn end_of_month(self) -> NanoTimestamp {
1019 use chrono::{Datelike, TimeZone};
1020 let dt = chrono::Utc.timestamp_nanos(self.0);
1021 let (next_year, next_month) = if dt.month() == 12 {
1023 (dt.year() + 1, 1u32)
1024 } else {
1025 (dt.year(), dt.month() + 1)
1026 };
1027 let start_of_next = chrono::Utc
1028 .with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
1029 .single()
1030 .unwrap_or(dt);
1031 let nanos = start_of_next.timestamp_nanos_opt().unwrap_or(self.0) - 1;
1032 NanoTimestamp(nanos)
1033 }
1034
1035 pub fn floor_to_second(self) -> NanoTimestamp {
1037 const NANOS_PER_SECOND: i64 = 1_000_000_000;
1038 NanoTimestamp((self.0 / NANOS_PER_SECOND) * NANOS_PER_SECOND)
1039 }
1040
1041 pub fn is_same_hour(self, other: NanoTimestamp) -> bool {
1043 use chrono::{Datelike, TimeZone, Timelike};
1044 let a = chrono::Utc.timestamp_nanos(self.0);
1045 let b = chrono::Utc.timestamp_nanos(other.0);
1046 a.year() == b.year() && a.month() == b.month() && a.day() == b.day() && a.hour() == b.hour()
1047 }
1048
1049 pub fn add_weeks(&self, weeks: i64) -> NanoTimestamp {
1051 const NANOS_PER_WEEK: i64 = 7 * 24 * 3_600 * 1_000_000_000;
1052 NanoTimestamp(self.0 + weeks * NANOS_PER_WEEK)
1053 }
1054
1055 pub fn sub_hours(&self, hours: i64) -> NanoTimestamp {
1057 const NANOS_PER_HOUR: i64 = 3_600 * 1_000_000_000;
1058 NanoTimestamp(self.0 - hours * NANOS_PER_HOUR)
1059 }
1060
1061 pub fn sub_weeks(&self, weeks: i64) -> NanoTimestamp {
1063 const NANOS_PER_WEEK: i64 = 7 * 24 * 3_600 * 1_000_000_000;
1064 NanoTimestamp(self.0 - weeks * NANOS_PER_WEEK)
1065 }
1066
1067 pub fn sub_seconds(&self, secs: i64) -> NanoTimestamp {
1069 const NANOS_PER_SECOND: i64 = 1_000_000_000;
1070 NanoTimestamp(self.0 - secs * NANOS_PER_SECOND)
1071 }
1072
1073 pub fn to_time_string(&self) -> String {
1075 use chrono::{TimeZone, Timelike};
1076 let dt = chrono::Utc.timestamp_nanos(self.0);
1077 format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
1078 }
1079
1080 pub fn elapsed_hours(&self, other: NanoTimestamp) -> f64 {
1082 let diff = (self.0 - other.0).unsigned_abs();
1083 diff as f64 / (3_600.0 * 1_000_000_000.0)
1084 }
1085
1086 pub fn is_today(&self, other: NanoTimestamp) -> bool {
1088 self.is_same_day(other)
1089 }
1090
1091 pub fn nanoseconds_between(self, other: NanoTimestamp) -> u64 {
1093 (self.0 - other.0).unsigned_abs()
1094 }
1095
1096 pub fn elapsed_minutes(&self, other: NanoTimestamp) -> f64 {
1098 let diff = (self.0 - other.0).unsigned_abs();
1099 diff as f64 / (60.0 * 1_000_000_000.0)
1100 }
1101
1102 pub fn elapsed_days(&self, other: NanoTimestamp) -> f64 {
1104 let diff = (self.0 - other.0).unsigned_abs();
1105 diff as f64 / (86_400.0 * 1_000_000_000.0)
1106 }
1107
1108 pub fn sub_nanos(&self, nanos: i64) -> NanoTimestamp {
1110 NanoTimestamp(self.0 - nanos)
1111 }
1112
1113 pub fn start_of_year(self) -> NanoTimestamp {
1115 use chrono::{Datelike, TimeZone};
1116 let dt = chrono::Utc.timestamp_nanos(self.0);
1117 let start = chrono::Utc
1118 .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
1119 .single()
1120 .unwrap_or(dt);
1121 NanoTimestamp(start.timestamp_nanos_opt().unwrap_or(self.0))
1122 }
1123
1124 pub fn end_of_year(self) -> NanoTimestamp {
1126 use chrono::{Datelike, TimeZone};
1127 let dt = chrono::Utc.timestamp_nanos(self.0);
1128 let start_next = chrono::Utc
1129 .with_ymd_and_hms(dt.year() + 1, 1, 1, 0, 0, 0)
1130 .single()
1131 .unwrap_or(dt);
1132 let nanos = start_next.timestamp_nanos_opt().unwrap_or(self.0) - 1;
1133 NanoTimestamp(nanos)
1134 }
1135
1136 pub fn add_months(&self, months: i32) -> NanoTimestamp {
1138 use chrono::{Datelike, TimeZone};
1139 let dt = chrono::Utc.timestamp_nanos(self.0);
1140 let total_months = dt.month() as i32 + months;
1141 let year = dt.year() + (total_months - 1).div_euclid(12);
1142 let month = ((total_months - 1).rem_euclid(12) + 1) as u32;
1143 let day = dt.day().min(days_in_month(year, month));
1144 let new_dt = chrono::Utc
1145 .with_ymd_and_hms(year, month, day, dt.hour(), dt.minute(), dt.second())
1146 .single()
1147 .unwrap_or(dt);
1148 NanoTimestamp(new_dt.timestamp_nanos_opt().unwrap_or(self.0))
1149 }
1150
1151 pub fn start_of_quarter(self) -> NanoTimestamp {
1154 use chrono::{Datelike, TimeZone};
1155 let dt = chrono::Utc.timestamp_nanos(self.0);
1156 let quarter_start_month = ((dt.month() - 1) / 3) * 3 + 1;
1157 chrono::Utc
1158 .with_ymd_and_hms(dt.year(), quarter_start_month, 1, 0, 0, 0)
1159 .single()
1160 .map(|d| NanoTimestamp(d.timestamp_nanos_opt().unwrap_or(self.0)))
1161 .unwrap_or(self)
1162 }
1163
1164 pub fn end_of_quarter(self) -> NanoTimestamp {
1166 use chrono::{Datelike, TimeZone};
1167 let dt = chrono::Utc.timestamp_nanos(self.0);
1168 let quarter_end_month = ((dt.month() - 1) / 3) * 3 + 3;
1169 let last_day = days_in_month(dt.year(), quarter_end_month);
1170 chrono::Utc
1171 .with_ymd_and_hms(dt.year(), quarter_end_month, last_day, 23, 59, 59)
1172 .single()
1173 .map(|d| NanoTimestamp(d.timestamp_nanos_opt().unwrap_or(self.0) + 999_999_999))
1174 .unwrap_or(self)
1175 }
1176
1177 pub fn is_same_quarter(self, other: NanoTimestamp) -> bool {
1179 use chrono::{Datelike, TimeZone};
1180 let a = chrono::Utc.timestamp_nanos(self.0);
1181 let b = chrono::Utc.timestamp_nanos(other.0);
1182 a.year() == b.year() && ((a.month() - 1) / 3) == ((b.month() - 1) / 3)
1183 }
1184}
1185
1186fn days_in_month(year: i32, month: u32) -> u32 {
1187 match month {
1188 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1189 4 | 6 | 9 | 11 => 30,
1190 2 if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) => 29,
1191 2 => 28,
1192 _ => 30,
1193 }
1194}
1195
1196impl std::ops::Add<i64> for NanoTimestamp {
1198 type Output = NanoTimestamp;
1199 fn add(self, rhs: i64) -> NanoTimestamp {
1200 NanoTimestamp(self.0 + rhs)
1201 }
1202}
1203
1204impl std::ops::Sub<i64> for NanoTimestamp {
1206 type Output = NanoTimestamp;
1207 fn sub(self, rhs: i64) -> NanoTimestamp {
1208 NanoTimestamp(self.0 - rhs)
1209 }
1210}
1211
1212impl std::ops::Sub<NanoTimestamp> for NanoTimestamp {
1214 type Output = i64;
1215 fn sub(self, rhs: NanoTimestamp) -> i64 {
1216 self.0 - rhs.0
1217 }
1218}
1219
1220impl std::fmt::Display for NanoTimestamp {
1221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1222 write!(f, "{}", self.0)
1223 }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::*;
1229 use rust_decimal_macros::dec;
1230
1231 #[test]
1234 fn test_symbol_new_valid_ok() {
1235 let sym = Symbol::new("AAPL").unwrap();
1236 assert_eq!(sym.as_str(), "AAPL");
1237 }
1238
1239 #[test]
1240 fn test_symbol_new_empty_fails() {
1241 let result = Symbol::new("");
1242 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1243 }
1244
1245 #[test]
1246 fn test_symbol_new_whitespace_fails() {
1247 let result = Symbol::new("AA PL");
1248 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1249 }
1250
1251 #[test]
1252 fn test_symbol_new_leading_whitespace_fails() {
1253 let result = Symbol::new(" AAPL");
1254 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1255 }
1256
1257 #[test]
1258 fn test_symbol_display() {
1259 let sym = Symbol::new("TSLA").unwrap();
1260 assert_eq!(format!("{sym}"), "TSLA");
1261 }
1262
1263 #[test]
1264 fn test_symbol_clone_equality() {
1265 let a = Symbol::new("BTC").unwrap();
1266 let b = a.clone();
1267 assert_eq!(a, b);
1268 }
1269
1270 #[test]
1271 fn test_symbol_arc_clone_is_cheap() {
1272 let a = Symbol::new("ETH").unwrap();
1273 let b = a.clone();
1274 assert_eq!(a.as_str().as_ptr(), b.as_str().as_ptr());
1275 }
1276
1277 #[test]
1280 fn test_price_new_positive_ok() {
1281 let p = Price::new(dec!(100.5)).unwrap();
1282 assert_eq!(p.value(), dec!(100.5));
1283 }
1284
1285 #[test]
1286 fn test_price_new_zero_fails() {
1287 let result = Price::new(dec!(0));
1288 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
1289 }
1290
1291 #[test]
1292 fn test_price_new_negative_fails() {
1293 let result = Price::new(dec!(-1));
1294 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
1295 }
1296
1297 #[test]
1298 fn test_price_ordering() {
1299 let p1 = Price::new(dec!(1)).unwrap();
1300 let p2 = Price::new(dec!(2)).unwrap();
1301 assert!(p1 < p2);
1302 }
1303
1304 #[test]
1305 fn test_price_add() {
1306 let a = Price::new(dec!(10)).unwrap();
1307 let b = Price::new(dec!(5)).unwrap();
1308 assert_eq!(a + b, dec!(15));
1309 }
1310
1311 #[test]
1312 fn test_price_sub() {
1313 let a = Price::new(dec!(10)).unwrap();
1314 let b = Price::new(dec!(3)).unwrap();
1315 assert_eq!(a - b, dec!(7));
1316 }
1317
1318 #[test]
1319 fn test_price_mul_quantity() {
1320 let p = Price::new(dec!(10)).unwrap();
1321 let q = Quantity::new(dec!(5)).unwrap();
1322 assert_eq!(p * q, dec!(50));
1323 }
1324
1325 #[test]
1326 fn test_price_mul_decimal_valid() {
1327 let p = Price::new(dec!(10)).unwrap();
1328 assert_eq!((p * dec!(2)).unwrap().value(), dec!(20));
1329 }
1330
1331 #[test]
1332 fn test_price_mul_decimal_zero_returns_none() {
1333 let p = Price::new(dec!(10)).unwrap();
1334 assert!((p * dec!(0)).is_none());
1335 }
1336
1337 #[test]
1340 fn test_quantity_new_zero_ok() {
1341 let q = Quantity::new(dec!(0)).unwrap();
1342 assert_eq!(q.value(), dec!(0));
1343 }
1344
1345 #[test]
1346 fn test_quantity_new_positive_ok() {
1347 let q = Quantity::new(dec!(5.5)).unwrap();
1348 assert_eq!(q.value(), dec!(5.5));
1349 }
1350
1351 #[test]
1352 fn test_quantity_new_negative_fails() {
1353 let result = Quantity::new(dec!(-0.01));
1354 assert!(matches!(result, Err(FinError::InvalidQuantity(_))));
1355 }
1356
1357 #[test]
1358 fn test_quantity_zero_constructor() {
1359 let q = Quantity::zero();
1360 assert_eq!(q.value(), Decimal::ZERO);
1361 }
1362
1363 #[test]
1364 fn test_quantity_add() {
1365 let a = Quantity::new(dec!(3)).unwrap();
1366 let b = Quantity::new(dec!(4)).unwrap();
1367 assert_eq!((a + b).value(), dec!(7));
1368 }
1369
1370 #[test]
1371 fn test_quantity_sub_positive() {
1372 let a = Quantity::new(dec!(10)).unwrap();
1373 let b = Quantity::new(dec!(3)).unwrap();
1374 assert_eq!(a - b, dec!(7));
1375 }
1376
1377 #[test]
1378 fn test_quantity_sub_negative() {
1379 let a = Quantity::new(dec!(3)).unwrap();
1380 let b = Quantity::new(dec!(10)).unwrap();
1381 assert_eq!(a - b, dec!(-7));
1382 }
1383
1384 #[test]
1385 fn test_quantity_mul_decimal() {
1386 let q = Quantity::new(dec!(5)).unwrap();
1387 assert_eq!(q * dec!(3), dec!(15));
1388 }
1389
1390 #[test]
1391 fn test_quantity_is_zero() {
1392 assert!(Quantity::zero().is_zero());
1393 assert!(!Quantity::new(dec!(1)).unwrap().is_zero());
1394 }
1395
1396 #[test]
1399 fn test_side_display_bid() {
1400 assert_eq!(format!("{}", Side::Bid), "Bid");
1401 }
1402
1403 #[test]
1404 fn test_side_display_ask() {
1405 assert_eq!(format!("{}", Side::Ask), "Ask");
1406 }
1407
1408 #[test]
1411 fn test_nano_timestamp_now_positive() {
1412 let ts = NanoTimestamp::now();
1413 assert!(ts.nanos() > 0);
1414 }
1415
1416 #[test]
1417 fn test_nano_timestamp_ordering() {
1418 let ts1 = NanoTimestamp::new(1_000_000_000);
1419 let ts2 = NanoTimestamp::new(2_000_000_000);
1420 assert!(ts1 < ts2);
1421 }
1422
1423 #[test]
1424 fn test_nano_timestamp_to_datetime_epoch() {
1425 let ts = NanoTimestamp::new(0);
1426 let dt = ts.to_datetime();
1427 assert_eq!(dt.timestamp(), 0);
1428 }
1429
1430 #[test]
1431 fn test_nano_timestamp_to_datetime_roundtrip() {
1432 let ts = NanoTimestamp::new(1_700_000_000_000_000_000_i64);
1433 let dt = ts.to_datetime();
1434 assert_eq!(
1435 dt.timestamp_nanos_opt().unwrap_or(0),
1436 1_700_000_000_000_000_000_i64
1437 );
1438 }
1439
1440 #[test]
1441 fn test_nano_timestamp_nanos_roundtrip() {
1442 let ts = NanoTimestamp::new(42_000_000);
1443 assert_eq!(ts.nanos(), 42_000_000);
1444 }
1445
1446 #[test]
1447 fn test_nano_timestamp_duration_since_positive() {
1448 let a = NanoTimestamp::new(1_000);
1449 let b = NanoTimestamp::new(600);
1450 assert_eq!(a.duration_since(b), 400);
1451 }
1452
1453 #[test]
1454 fn test_nano_timestamp_duration_since_negative() {
1455 let a = NanoTimestamp::new(500);
1456 let b = NanoTimestamp::new(1_000);
1457 assert_eq!(a.duration_since(b), -500);
1458 }
1459
1460 #[test]
1461 fn test_symbol_len() {
1462 let sym = Symbol::new("AAPL").unwrap();
1463 assert_eq!(sym.len(), 4);
1464 }
1465
1466 #[test]
1467 fn test_symbol_is_empty_always_false() {
1468 let sym = Symbol::new("X").unwrap();
1469 assert!(!sym.is_empty());
1470 }
1471
1472 #[test]
1473 fn test_symbol_try_from_string_valid() {
1474 let sym = Symbol::try_from("AAPL".to_owned()).unwrap();
1475 assert_eq!(sym.as_str(), "AAPL");
1476 }
1477
1478 #[test]
1479 fn test_symbol_try_from_str_valid() {
1480 let sym = Symbol::try_from("ETH").unwrap();
1481 assert_eq!(sym.as_str(), "ETH");
1482 }
1483
1484 #[test]
1485 fn test_symbol_try_from_empty_fails() {
1486 assert!(Symbol::try_from("").is_err());
1487 }
1488
1489 #[test]
1490 fn test_symbol_try_from_whitespace_fails() {
1491 assert!(Symbol::try_from("BTC USD").is_err());
1492 }
1493
1494 #[test]
1495 fn test_nano_timestamp_from_datetime_roundtrip() {
1496 let original = NanoTimestamp::new(1_700_000_000_000_000_000_i64);
1497 let dt = original.to_datetime();
1498 let recovered = NanoTimestamp::from_datetime(dt);
1499 assert_eq!(recovered.nanos(), original.nanos());
1500 }
1501
1502 #[test]
1503 fn test_nano_timestamp_from_datetime_epoch() {
1504 use chrono::Utc;
1505 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
1506 let ts = NanoTimestamp::from_datetime(epoch);
1507 assert_eq!(ts.nanos(), 0);
1508 }
1509
1510 #[test]
1511 fn test_price_to_f64() {
1512 let p = Price::new(dec!(123.45)).unwrap();
1513 let f = p.to_f64();
1514 assert!((f - 123.45_f64).abs() < 1e-6);
1515 }
1516
1517 #[test]
1518 fn test_quantity_to_f64() {
1519 let q = Quantity::new(dec!(42)).unwrap();
1520 assert!((q.to_f64() - 42.0_f64).abs() < 1e-10);
1521 }
1522
1523 #[test]
1524 fn test_price_from_f64_valid() {
1525 let p = Price::from_f64(42.5).unwrap();
1526 assert!((p.to_f64() - 42.5).abs() < 1e-6);
1527 }
1528
1529 #[test]
1530 fn test_price_from_f64_zero_returns_none() {
1531 assert!(Price::from_f64(0.0).is_none());
1532 }
1533
1534 #[test]
1535 fn test_price_from_f64_negative_returns_none() {
1536 assert!(Price::from_f64(-1.0).is_none());
1537 }
1538
1539 #[test]
1540 fn test_quantity_from_f64_valid() {
1541 let q = Quantity::from_f64(10.0).unwrap();
1542 assert!((q.to_f64() - 10.0).abs() < 1e-10);
1543 }
1544
1545 #[test]
1546 fn test_quantity_from_f64_zero_valid() {
1547 let q = Quantity::from_f64(0.0).unwrap();
1548 assert!(q.is_zero());
1549 }
1550
1551 #[test]
1552 fn test_quantity_from_f64_negative_returns_none() {
1553 assert!(Quantity::from_f64(-1.0).is_none());
1554 }
1555
1556 #[test]
1557 fn test_nano_timestamp_add_millis() {
1558 let ts = NanoTimestamp::new(0);
1559 assert_eq!(ts.add_millis(1).nanos(), 1_000_000);
1560 }
1561
1562 #[test]
1563 fn test_nano_timestamp_add_seconds() {
1564 let ts = NanoTimestamp::new(0);
1565 assert_eq!(ts.add_seconds(2).nanos(), 2_000_000_000);
1566 }
1567
1568 #[test]
1569 fn test_nano_timestamp_is_before_after() {
1570 let a = NanoTimestamp::new(1_000);
1571 let b = NanoTimestamp::new(2_000);
1572 assert!(a.is_before(b));
1573 assert!(b.is_after(a));
1574 assert!(!a.is_after(b));
1575 assert!(!b.is_before(a));
1576 }
1577
1578 #[test]
1579 fn test_nano_timestamp_from_secs_roundtrip() {
1580 let ts = NanoTimestamp::from_secs(1_700_000_000);
1581 assert_eq!(ts.to_secs(), 1_700_000_000);
1582 }
1583
1584 #[test]
1585 fn test_nano_timestamp_from_secs_truncates_sub_second() {
1586 let ts = NanoTimestamp::new(1_700_000_000_999_999_999);
1587 assert_eq!(ts.to_secs(), 1_700_000_000);
1588 }
1589
1590 #[test]
1591 fn test_symbol_ord_lexicographic() {
1592 let a = Symbol::new("AAPL").unwrap();
1593 let b = Symbol::new("MSFT").unwrap();
1594 let c = Symbol::new("AAPL").unwrap();
1595 assert!(a < b);
1596 assert!(b > a);
1597 assert_eq!(a.cmp(&c), std::cmp::Ordering::Equal);
1598 }
1599
1600 #[test]
1601 fn test_symbol_ord_usable_in_btreemap() {
1602 use std::collections::BTreeMap;
1603 let mut m: BTreeMap<Symbol, i32> = BTreeMap::new();
1604 m.insert(Symbol::new("Z").unwrap(), 3);
1605 m.insert(Symbol::new("A").unwrap(), 1);
1606 m.insert(Symbol::new("M").unwrap(), 2);
1607 let keys: Vec<_> = m.keys().map(|s| s.as_str()).collect();
1608 assert_eq!(keys, ["A", "M", "Z"]);
1609 }
1610
1611 #[test]
1612 fn test_price_pct_change_positive() {
1613 let p1 = Price::new(dec!(100)).unwrap();
1614 let p2 = Price::new(dec!(110)).unwrap();
1615 assert_eq!(p1.pct_change_to(p2), dec!(10));
1616 }
1617
1618 #[test]
1619 fn test_price_pct_change_negative() {
1620 let p1 = Price::new(dec!(100)).unwrap();
1621 let p2 = Price::new(dec!(90)).unwrap();
1622 assert_eq!(p1.pct_change_to(p2), dec!(-10));
1623 }
1624
1625 #[test]
1626 fn test_price_pct_change_zero() {
1627 let p = Price::new(dec!(100)).unwrap();
1628 assert_eq!(p.pct_change_to(p), dec!(0));
1629 }
1630
1631 #[test]
1632 fn test_nano_timestamp_elapsed_is_non_negative_for_past() {
1633 let past = NanoTimestamp::new(0); assert!(past.elapsed() > 0);
1635 }
1636
1637 #[test]
1638 fn test_price_checked_mul_some() {
1639 let p = Price::new(dec!(100)).unwrap();
1640 let q = Quantity::new(dec!(5)).unwrap();
1641 assert_eq!(p.checked_mul(q), Some(dec!(500)));
1642 }
1643
1644 #[test]
1645 fn test_price_checked_mul_with_zero_qty() {
1646 let p = Price::new(dec!(100)).unwrap();
1647 let q = Quantity::zero();
1648 assert_eq!(p.checked_mul(q), Some(dec!(0)));
1649 }
1650
1651 #[test]
1652 fn test_quantity_checked_add() {
1653 let a = Quantity::new(dec!(10)).unwrap();
1654 let b = Quantity::new(dec!(5)).unwrap();
1655 assert_eq!(a.checked_add(b).map(|q| q.value()), Some(dec!(15)));
1656 }
1657
1658 #[test]
1659 fn test_nano_timestamp_min_less_than_max() {
1660 assert!(NanoTimestamp::MIN < NanoTimestamp::MAX);
1661 assert!(NanoTimestamp::MIN < NanoTimestamp::new(0));
1662 assert!(NanoTimestamp::new(0) < NanoTimestamp::MAX);
1663 }
1664
1665 #[test]
1666 fn test_price_midpoint() {
1667 let bid = Price::new(dec!(99)).unwrap();
1668 let ask = Price::new(dec!(101)).unwrap();
1669 assert_eq!(Price::midpoint(bid, ask), dec!(100));
1670 }
1671
1672 #[test]
1673 fn test_price_midpoint_same_price() {
1674 let p = Price::new(dec!(100)).unwrap();
1675 assert_eq!(Price::midpoint(p, p), dec!(100));
1676 }
1677
1678 #[test]
1679 fn test_price_mid_method() {
1680 let bid = Price::new(dec!(100)).unwrap();
1681 let ask = Price::new(dec!(102)).unwrap();
1682 let mid = bid.mid(ask);
1683 assert_eq!(mid.value(), dec!(101));
1684 }
1685
1686 #[test]
1687 fn test_price_mid_method_same_price() {
1688 let p = Price::new(dec!(100)).unwrap();
1689 assert_eq!(p.mid(p).value(), dec!(100));
1690 }
1691
1692 #[test]
1693 fn test_price_abs_diff_positive() {
1694 let a = Price::new(dec!(105)).unwrap();
1695 let b = Price::new(dec!(100)).unwrap();
1696 assert_eq!(a.abs_diff(b), dec!(5));
1697 assert_eq!(b.abs_diff(a), dec!(5));
1698 }
1699
1700 #[test]
1701 fn test_price_abs_diff_same() {
1702 let p = Price::new(dec!(100)).unwrap();
1703 assert_eq!(p.abs_diff(p), dec!(0));
1704 }
1705
1706 #[test]
1707 fn test_quantity_checked_sub_valid() {
1708 let a = Quantity::new(dec!(10)).unwrap();
1709 let b = Quantity::new(dec!(3)).unwrap();
1710 assert_eq!(a.checked_sub(b).unwrap().value(), dec!(7));
1711 }
1712
1713 #[test]
1714 fn test_quantity_checked_sub_exact_zero() {
1715 let a = Quantity::new(dec!(5)).unwrap();
1716 let b = Quantity::new(dec!(5)).unwrap();
1717 assert_eq!(a.checked_sub(b).unwrap().value(), dec!(0));
1718 }
1719
1720 #[test]
1721 fn test_quantity_checked_sub_negative_returns_none() {
1722 let a = Quantity::new(dec!(3)).unwrap();
1723 let b = Quantity::new(dec!(5)).unwrap();
1724 assert!(a.checked_sub(b).is_none());
1725 }
1726
1727 #[test]
1728 fn test_nano_timestamp_min_returns_earlier() {
1729 let t1 = NanoTimestamp::new(100);
1730 let t2 = NanoTimestamp::new(200);
1731 assert_eq!(t1.min(t2), t1);
1732 assert_eq!(t2.min(t1), t1);
1733 }
1734
1735 #[test]
1736 fn test_nano_timestamp_max_returns_later() {
1737 let t1 = NanoTimestamp::new(100);
1738 let t2 = NanoTimestamp::new(200);
1739 assert_eq!(t1.max(t2), t2);
1740 assert_eq!(t2.max(t1), t2);
1741 }
1742
1743 #[test]
1744 fn test_nano_timestamp_min_max_same() {
1745 let t = NanoTimestamp::new(500);
1746 assert_eq!(t.min(t), t);
1747 assert_eq!(t.max(t), t);
1748 }
1749
1750 #[test]
1751 fn test_side_opposite_bid() {
1752 assert_eq!(Side::Bid.opposite(), Side::Ask);
1753 }
1754
1755 #[test]
1756 fn test_side_opposite_ask() {
1757 assert_eq!(Side::Ask.opposite(), Side::Bid);
1758 }
1759
1760 #[test]
1761 fn test_side_opposite_involution() {
1762 assert_eq!(Side::Bid.opposite().opposite(), Side::Bid);
1763 }
1764
1765 #[test]
1766 fn test_price_checked_add_valid() {
1767 let a = Price::new(dec!(100)).unwrap();
1768 let b = Price::new(dec!(50)).unwrap();
1769 assert_eq!(a.checked_add(b).unwrap().value(), dec!(150));
1770 }
1771
1772 #[test]
1773 fn test_price_checked_add_result_validated() {
1774 let a = Price::new(dec!(1)).unwrap();
1776 let b = Price::new(dec!(2)).unwrap();
1777 assert!(a.checked_add(b).is_some());
1778 }
1779
1780 #[test]
1781 fn test_price_lerp_midpoint() {
1782 let a = Price::new(dec!(100)).unwrap();
1783 let b = Price::new(dec!(200)).unwrap();
1784 let mid = a.lerp(b, dec!(0.5)).unwrap();
1785 assert_eq!(mid.value(), dec!(150));
1786 }
1787
1788 #[test]
1789 fn test_price_lerp_at_zero_returns_self() {
1790 let a = Price::new(dec!(100)).unwrap();
1791 let b = Price::new(dec!(200)).unwrap();
1792 assert_eq!(a.lerp(b, Decimal::ZERO).unwrap().value(), dec!(100));
1793 }
1794
1795 #[test]
1796 fn test_price_lerp_at_one_returns_other() {
1797 let a = Price::new(dec!(100)).unwrap();
1798 let b = Price::new(dec!(200)).unwrap();
1799 assert_eq!(a.lerp(b, Decimal::ONE).unwrap().value(), dec!(200));
1800 }
1801
1802 #[test]
1803 fn test_price_lerp_out_of_range_returns_none() {
1804 let a = Price::new(dec!(100)).unwrap();
1805 let b = Price::new(dec!(200)).unwrap();
1806 assert!(a.lerp(b, dec!(1.5)).is_none());
1807 assert!(a.lerp(b, dec!(-0.1)).is_none());
1808 }
1809
1810 #[test]
1811 fn test_quantity_scale_half() {
1812 let q = Quantity::new(dec!(100)).unwrap();
1813 let result = q.scale(dec!(0.5)).unwrap();
1814 assert_eq!(result.value(), dec!(50));
1815 }
1816
1817 #[test]
1818 fn test_quantity_scale_zero_factor() {
1819 let q = Quantity::new(dec!(100)).unwrap();
1820 let result = q.scale(Decimal::ZERO).unwrap();
1821 assert_eq!(result.value(), dec!(0));
1822 }
1823
1824 #[test]
1825 fn test_quantity_scale_negative_factor_returns_none() {
1826 let q = Quantity::new(dec!(100)).unwrap();
1827 assert!(q.scale(dec!(-1)).is_none());
1828 }
1829
1830 #[test]
1831 fn test_nano_timestamp_elapsed_since_positive() {
1832 let earlier = NanoTimestamp::new(1000);
1833 let later = NanoTimestamp::new(3000);
1834 assert_eq!(later.elapsed_since(earlier), 2000);
1835 }
1836
1837 #[test]
1838 fn test_nano_timestamp_elapsed_since_negative() {
1839 let earlier = NanoTimestamp::new(1000);
1840 let later = NanoTimestamp::new(3000);
1841 assert_eq!(earlier.elapsed_since(later), -2000);
1843 }
1844
1845 #[test]
1846 fn test_nano_timestamp_elapsed_since_same_is_zero() {
1847 let ts = NanoTimestamp::new(5000);
1848 assert_eq!(ts.elapsed_since(ts), 0);
1849 }
1850
1851 #[test]
1852 fn test_nano_timestamp_to_seconds_one_second() {
1853 let ts = NanoTimestamp::new(1_000_000_000);
1854 assert!((ts.to_seconds() - 1.0_f64).abs() < 1e-9);
1855 }
1856
1857 #[test]
1858 fn test_nano_timestamp_to_seconds_zero() {
1859 let ts = NanoTimestamp::new(0);
1860 assert_eq!(ts.to_seconds(), 0.0);
1861 }
1862
1863 #[test]
1864 fn test_quantity_split_even() {
1865 let q = Quantity::new(dec!(10)).unwrap();
1866 let parts = q.split(5);
1867 assert_eq!(parts.len(), 5);
1868 let total: Decimal = parts.iter().map(|p| p.value()).sum();
1869 assert_eq!(total, dec!(10));
1870 }
1871
1872 #[test]
1873 fn test_quantity_split_remainder_goes_to_last() {
1874 let q = Quantity::new(dec!(10)).unwrap();
1875 let parts = q.split(3);
1876 assert_eq!(parts.len(), 3);
1877 let total: Decimal = parts.iter().map(|p| p.value()).sum();
1878 assert_eq!(total, dec!(10));
1879 }
1880
1881 #[test]
1882 fn test_quantity_split_zero_n_returns_empty() {
1883 let q = Quantity::new(dec!(10)).unwrap();
1884 assert!(q.split(0).is_empty());
1885 }
1886
1887 #[test]
1888 fn test_quantity_split_one_returns_self() {
1889 let q = Quantity::new(dec!(10)).unwrap();
1890 let parts = q.split(1);
1891 assert_eq!(parts.len(), 1);
1892 assert_eq!(parts[0].value(), dec!(10));
1893 }
1894
1895 #[test]
1896 fn test_price_pct_move_up() {
1897 let p = Price::new(dec!(100)).unwrap();
1898 let result = p.pct_move(dec!(10)).unwrap();
1899 assert_eq!(result.value(), dec!(110));
1900 }
1901
1902 #[test]
1903 fn test_price_pct_move_down() {
1904 let p = Price::new(dec!(100)).unwrap();
1905 let result = p.pct_move(dec!(-10)).unwrap();
1906 assert_eq!(result.value(), dec!(90));
1907 }
1908
1909 #[test]
1910 fn test_price_pct_move_negative_to_invalid() {
1911 let p = Price::new(dec!(100)).unwrap();
1912 assert!(p.pct_move(dec!(-100)).is_none());
1914 }
1915
1916 #[test]
1917 fn test_quantity_proportion_of_half() {
1918 let a = Quantity::new(dec!(5)).unwrap();
1919 let total = Quantity::new(dec!(10)).unwrap();
1920 assert_eq!(a.proportion_of(total), Some(dec!(0.5)));
1921 }
1922
1923 #[test]
1924 fn test_quantity_proportion_of_zero_total_returns_none() {
1925 let a = Quantity::new(dec!(5)).unwrap();
1926 let total = Quantity::zero();
1927 assert!(a.proportion_of(total).is_none());
1928 }
1929
1930 #[test]
1931 fn test_nano_timestamp_duration_millis() {
1932 let a = NanoTimestamp::new(0);
1933 let b = NanoTimestamp::new(1_500_000_000); assert_eq!(b.duration_millis(a), 1500);
1935 }
1936
1937 #[test]
1938 fn test_nano_timestamp_duration_millis_negative() {
1939 let a = NanoTimestamp::new(0);
1940 let b = NanoTimestamp::new(2_000_000_000);
1941 assert_eq!(a.duration_millis(b), -2000);
1942 }
1943
1944 #[test]
1945 fn test_nano_timestamp_minutes_since_positive() {
1946 let a = NanoTimestamp::new(0);
1947 let b = NanoTimestamp::new(3 * 60_000_000_000i64);
1948 assert_eq!(b.minutes_since(a), 3);
1949 }
1950
1951 #[test]
1952 fn test_nano_timestamp_minutes_since_negative() {
1953 let a = NanoTimestamp::new(0);
1954 let b = NanoTimestamp::new(3 * 60_000_000_000i64);
1955 assert_eq!(a.minutes_since(b), -3);
1956 }
1957
1958 #[test]
1959 fn test_nano_timestamp_hours_since_positive() {
1960 let a = NanoTimestamp::new(0);
1961 let b = NanoTimestamp::new(2 * 3_600_000_000_000i64);
1962 assert_eq!(b.hours_since(a), 2);
1963 }
1964
1965 #[test]
1966 fn test_nano_timestamp_hours_since_same_returns_zero() {
1967 let a = NanoTimestamp::new(1_000_000);
1968 assert_eq!(a.hours_since(a), 0);
1969 }
1970
1971 #[test]
1972 fn test_price_is_within_pct_same_price() {
1973 let p = Price::new(dec!(100)).unwrap();
1974 assert!(p.is_within_pct(p, dec!(0)));
1975 }
1976
1977 #[test]
1978 fn test_price_is_within_pct_within_range() {
1979 let p = Price::new(dec!(100)).unwrap();
1980 let q = Price::new(dec!(101)).unwrap();
1981 assert!(p.is_within_pct(q, dec!(2)));
1982 }
1983
1984 #[test]
1985 fn test_price_is_within_pct_outside_range() {
1986 let p = Price::new(dec!(100)).unwrap();
1987 let q = Price::new(dec!(110)).unwrap();
1988 assert!(!p.is_within_pct(q, dec!(5)));
1989 }
1990
1991 #[test]
1992 fn test_price_is_within_pct_negative_pct_returns_false() {
1993 let p = Price::new(dec!(100)).unwrap();
1994 assert!(!p.is_within_pct(p, dec!(-1)));
1995 }
1996
1997 #[test]
1998 fn test_timestamp_is_between_inclusive() {
1999 let ts = NanoTimestamp::new(500);
2000 assert!(ts.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2001 assert!(ts.is_between(NanoTimestamp::new(500), NanoTimestamp::new(500))); }
2003
2004 #[test]
2005 fn test_timestamp_is_between_outside() {
2006 let ts = NanoTimestamp::new(50);
2007 assert!(!ts.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2008 let ts2 = NanoTimestamp::new(1000);
2009 assert!(!ts2.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2010 }
2011
2012 #[test]
2013 fn test_timestamp_to_unix_ms() {
2014 let ts = NanoTimestamp::new(1_500_000_000); assert_eq!(ts.to_unix_ms(), 1500);
2016 }
2017
2018 #[test]
2019 fn test_timestamp_to_unix_ms_truncates() {
2020 let ts = NanoTimestamp::new(1_999_999); assert_eq!(ts.to_unix_ms(), 1);
2022 }
2023
2024 #[test]
2025 fn test_price_round_to_tick_same_as_snap() {
2026 let p = Price::new(dec!(100.7)).unwrap();
2027 assert_eq!(p.round_to_tick(dec!(0.5)), p.snap_to_tick(dec!(0.5)));
2028 }
2029
2030 #[test]
2031 fn test_price_round_to_tick_invalid_tick_returns_none() {
2032 let p = Price::new(dec!(100)).unwrap();
2033 assert!(p.round_to_tick(dec!(0)).is_none());
2034 assert!(p.round_to_tick(dec!(-1)).is_none());
2035 }
2036
2037 #[test]
2038 fn test_nanotimestamp_day_of_week_epoch_is_thursday() {
2039 let ts = NanoTimestamp::new(0);
2041 assert_eq!(ts.day_of_week(), 3);
2042 }
2043
2044 #[test]
2045 fn test_nanotimestamp_day_of_week_next_day() {
2046 let ts = NanoTimestamp::new(86_400 * 1_000_000_000);
2048 assert_eq!(ts.day_of_week(), 4);
2049 }
2050
2051 #[test]
2052 fn test_nanotimestamp_sub_minutes_round_trip() {
2053 let ts = NanoTimestamp::new(3_600_000_000_000); let back = ts.sub_minutes(30);
2055 let forward = back.add_minutes(30);
2056 assert_eq!(forward.nanos(), ts.nanos());
2057 }
2058
2059 #[test]
2060 fn test_nanotimestamp_sub_minutes_by_zero() {
2061 let ts = NanoTimestamp::new(1_000_000_000);
2062 assert_eq!(ts.sub_minutes(0).nanos(), ts.nanos());
2063 }
2064}