1use crate::error::FinError;
20use chrono::{DateTime, TimeZone, Timelike, Utc};
21use rust_decimal::Decimal;
22use std::sync::Arc;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
39pub struct Symbol(Arc<str>);
40
41impl Symbol {
42 pub fn new(s: impl AsRef<str>) -> Result<Self, FinError> {
47 let s = s.as_ref();
48 if s.is_empty() || s.chars().any(char::is_whitespace) {
49 return Err(FinError::InvalidSymbol(s.to_owned()));
50 }
51 Ok(Self(Arc::from(s)))
52 }
53
54 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58
59 pub fn len(&self) -> usize {
61 self.0.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
69 self.0.is_empty()
70 }
71}
72
73impl std::fmt::Display for Symbol {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str(&self.0)
76 }
77}
78
79impl AsRef<str> for Symbol {
80 fn as_ref(&self) -> &str {
81 &self.0
82 }
83}
84
85impl std::borrow::Borrow<str> for Symbol {
86 fn borrow(&self) -> &str {
87 &self.0
88 }
89}
90
91impl TryFrom<String> for Symbol {
92 type Error = FinError;
93
94 fn try_from(s: String) -> Result<Self, Self::Error> {
95 Symbol::new(s)
96 }
97}
98
99impl TryFrom<&str> for Symbol {
100 type Error = FinError;
101
102 fn try_from(s: &str) -> Result<Self, Self::Error> {
103 Symbol::new(s)
104 }
105}
106
107impl PartialOrd for Symbol {
108 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
109 Some(self.cmp(other))
110 }
111}
112
113impl Ord for Symbol {
114 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115 self.0.as_ref().cmp(other.0.as_ref())
116 }
117}
118
119impl From<Symbol> for String {
120 fn from(s: Symbol) -> Self {
121 s.as_str().to_owned()
122 }
123}
124
125impl From<Symbol> for Arc<str> {
126 fn from(s: Symbol) -> Self {
127 s.0.clone()
128 }
129}
130
131#[derive(
141 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
142)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144#[cfg_attr(feature = "serde", serde(try_from = "Decimal", into = "Decimal"))]
146pub struct Price(Decimal);
147
148impl TryFrom<Decimal> for Price {
149 type Error = FinError;
150
151 fn try_from(d: Decimal) -> Result<Self, Self::Error> {
152 Price::new(d)
153 }
154}
155
156impl From<Price> for Decimal {
157 fn from(p: Price) -> Self {
158 p.0
159 }
160}
161
162impl Price {
163 pub fn new(d: Decimal) -> Result<Self, FinError> {
168 if d <= Decimal::ZERO {
169 return Err(FinError::InvalidPrice(d));
170 }
171 Ok(Self(d))
172 }
173
174 pub fn value(&self) -> Decimal {
176 self.0
177 }
178
179 pub fn to_f64(&self) -> f64 {
181 rust_decimal::prelude::ToPrimitive::to_f64(&self.0).unwrap_or(f64::NAN)
182 }
183
184 pub fn from_f64(f: f64) -> Option<Self> {
186 use rust_decimal::prelude::FromPrimitive;
187 let d = Decimal::from_f64(f)?;
188 Self::new(d).ok()
189 }
190
191 pub fn to_string_with_dp(&self, dp: u32) -> String {
195 self.0.round_dp(dp).to_string()
196 }
197}
198
199impl Price {
200 pub fn pct_change_to(self, other: Price) -> Decimal {
204 (other.0 - self.0) / self.0 * Decimal::ONE_HUNDRED
205 }
206
207 pub fn mid(self, other: Price) -> Price {
209 Price((self.0 + other.0) / Decimal::TWO)
210 }
211}
212
213impl Price {
214 pub fn abs_diff(self, other: Price) -> Decimal {
216 (self.0 - other.0).abs()
217 }
218
219 pub fn snap_to_tick(self, tick_size: Decimal) -> Option<Price> {
234 if tick_size <= Decimal::ZERO {
235 return None;
236 }
237 let rounded = (self.0 / tick_size).round() * tick_size;
238 Price::new(rounded).ok()
239 }
240
241 pub fn clamp(self, lo: Price, hi: Price) -> Price {
245 if self.0 < lo.0 {
246 lo
247 } else if self.0 > hi.0 {
248 hi
249 } else {
250 self
251 }
252 }
253}
254
255impl Price {
256 pub fn round_to(self, dp: u32) -> Option<Price> {
260 let rounded = self.0.round_dp(dp);
261 Price::new(rounded).ok()
262 }
263
264 pub fn round_half_up(self, dp: u32) -> Option<Price> {
269 use rust_decimal::RoundingStrategy;
270 let rounded = self.0.round_dp_with_strategy(dp, RoundingStrategy::MidpointAwayFromZero);
271 Price::new(rounded).ok()
272 }
273}
274
275impl Price {
276 pub fn checked_add(self, other: Price) -> Option<Price> {
280 let sum = self.0.checked_add(other.0)?;
281 Price::new(sum).ok()
282 }
283}
284
285impl Price {
286 pub fn checked_mul(self, qty: Quantity) -> Option<Decimal> {
291 self.0.checked_mul(qty.0)
292 }
293}
294
295impl Price {
296 pub fn midpoint(bid: Price, ask: Price) -> Decimal {
300 (bid.0 + ask.0) / Decimal::TWO
301 }
302
303 pub fn pct_move(self, pct: Decimal) -> Option<Price> {
308 let result = self.0 * (Decimal::ONE + pct / Decimal::ONE_HUNDRED);
309 Price::new(result).ok()
310 }
311
312 pub fn lerp(self, other: Price, t: Decimal) -> Option<Price> {
317 if t < Decimal::ZERO || t > Decimal::ONE {
318 return None;
319 }
320 let result = self.0 + (other.0 - self.0) * t;
321 Price::new(result).ok()
322 }
323
324 pub fn is_within_pct(self, other: Price, pct: Decimal) -> bool {
329 if pct < Decimal::ZERO {
330 return false;
331 }
332 let diff = (self.0 - other.0).abs();
333 diff / self.0 * Decimal::ONE_HUNDRED <= pct
334 }
335
336 pub fn distance_pct(self, other: Price) -> Decimal {
340 (other.0 - self.0) / self.0 * Decimal::ONE_HUNDRED
341 }
342
343 pub fn round_to_tick(self, tick_size: Decimal) -> Option<Price> {
349 self.snap_to_tick(tick_size)
350 }
351}
352
353impl std::fmt::Display for Price {
354 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355 write!(f, "{}", self.0)
356 }
357}
358
359impl std::ops::Add<Price> for Price {
361 type Output = Decimal;
362 fn add(self, rhs: Price) -> Decimal {
363 self.0 + rhs.0
364 }
365}
366
367impl std::ops::Sub<Price> for Price {
369 type Output = Decimal;
370 fn sub(self, rhs: Price) -> Decimal {
371 self.0 - rhs.0
372 }
373}
374
375impl std::ops::Mul<Quantity> for Price {
377 type Output = Decimal;
378 fn mul(self, rhs: Quantity) -> Decimal {
379 self.0 * rhs.0
380 }
381}
382
383impl std::ops::Mul<Decimal> for Price {
385 type Output = Option<Price>;
386 fn mul(self, rhs: Decimal) -> Option<Price> {
387 Price::new(self.0 * rhs).ok()
388 }
389}
390
391#[derive(
401 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
402)]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404#[cfg_attr(feature = "serde", serde(try_from = "Decimal", into = "Decimal"))]
406pub struct Quantity(Decimal);
407
408impl TryFrom<Decimal> for Quantity {
409 type Error = FinError;
410
411 fn try_from(d: Decimal) -> Result<Self, Self::Error> {
412 Quantity::new(d)
413 }
414}
415
416impl From<Quantity> for Decimal {
417 fn from(q: Quantity) -> Self {
418 q.0
419 }
420}
421
422impl Quantity {
423 pub fn new(d: Decimal) -> Result<Self, FinError> {
428 if d < Decimal::ZERO {
429 return Err(FinError::InvalidQuantity(d));
430 }
431 Ok(Self(d))
432 }
433
434 pub fn zero() -> Self {
436 Self(Decimal::ZERO)
437 }
438
439 pub fn is_zero(&self) -> bool {
441 self.0 == Decimal::ZERO
442 }
443
444 pub fn value(&self) -> Decimal {
446 self.0
447 }
448
449 pub fn to_f64(&self) -> f64 {
451 rust_decimal::prelude::ToPrimitive::to_f64(&self.0).unwrap_or(f64::NAN)
452 }
453
454 pub fn from_f64(f: f64) -> Option<Self> {
456 use rust_decimal::prelude::FromPrimitive;
457 let d = Decimal::from_f64(f)?;
458 Self::new(d).ok()
459 }
460}
461
462impl Quantity {
463 pub fn checked_add(self, other: Quantity) -> Option<Quantity> {
465 self.0.checked_add(other.0).map(Quantity)
466 }
467
468 pub fn checked_sub(self, other: Quantity) -> Option<Quantity> {
470 let result = self.0.checked_sub(other.0)?;
471 if result < Decimal::ZERO {
472 None
473 } else {
474 Some(Quantity(result))
475 }
476 }
477
478 pub fn abs(self) -> Quantity {
484 Quantity(self.0.abs())
485 }
486
487 pub fn split(self, n: usize) -> Vec<Quantity> {
492 if n == 0 {
493 return Vec::new();
494 }
495 let part = self.0 / Decimal::from(n as u64);
496 let mut parts: Vec<Quantity> = (0..n - 1).map(|_| Quantity(part)).collect();
497 let assigned: Decimal = part * Decimal::from((n - 1) as u64);
498 parts.push(Quantity(self.0 - assigned));
499 parts
500 }
501
502 pub fn proportion_of(self, total: Quantity) -> Option<Decimal> {
506 if total.is_zero() {
507 return None;
508 }
509 Some(self.0 / total.0)
510 }
511
512 pub fn scale(self, factor: Decimal) -> Option<Quantity> {
517 if factor < Decimal::ZERO {
518 return None;
519 }
520 Some(Quantity(self.0 * factor))
521 }
522}
523
524impl std::fmt::Display for Quantity {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 write!(f, "{}", self.0)
527 }
528}
529
530impl std::ops::Add<Quantity> for Quantity {
532 type Output = Quantity;
533 fn add(self, rhs: Quantity) -> Quantity {
534 Quantity(self.0 + rhs.0)
535 }
536}
537
538impl std::ops::Sub<Quantity> for Quantity {
540 type Output = Decimal;
541 fn sub(self, rhs: Quantity) -> Decimal {
542 self.0 - rhs.0
543 }
544}
545
546impl std::ops::Mul<Decimal> for Quantity {
548 type Output = Decimal;
549 fn mul(self, rhs: Decimal) -> Decimal {
550 self.0 * rhs
551 }
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557pub enum Side {
558 Bid,
560 Ask,
562}
563
564impl Side {
565 pub fn opposite(self) -> Side {
567 match self {
568 Side::Bid => Side::Ask,
569 Side::Ask => Side::Bid,
570 }
571 }
572}
573
574impl std::fmt::Display for Side {
575 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576 match self {
577 Side::Bid => f.write_str("Bid"),
578 Side::Ask => f.write_str("Ask"),
579 }
580 }
581}
582
583#[derive(
588 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
589)]
590#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
591pub struct NanoTimestamp(i64);
592
593impl NanoTimestamp {
594 pub const MIN: NanoTimestamp = NanoTimestamp(i64::MIN);
596
597 pub const MAX: NanoTimestamp = NanoTimestamp(i64::MAX);
599 pub fn new(nanos: i64) -> Self {
601 Self(nanos)
602 }
603
604 pub fn nanos(&self) -> i64 {
606 self.0
607 }
608
609 pub fn as_nanos(&self) -> u128 {
613 self.0.max(0) as u128
614 }
615
616 pub fn now() -> Self {
620 Self(Utc::now().timestamp_nanos_opt().unwrap_or(0))
621 }
622
623 pub fn elapsed(&self) -> i64 {
627 NanoTimestamp::now().0 - self.0
628 }
629
630 pub fn duration_since(&self, other: NanoTimestamp) -> i64 {
634 self.0 - other.0
635 }
636
637 pub fn diff_millis(&self, other: NanoTimestamp) -> i64 {
642 (self.0 - other.0) / 1_000_000
643 }
644
645 pub fn elapsed_nanos_since(&self, other: NanoTimestamp) -> Option<i64> {
650 let diff = self.0 - other.0;
651 if diff >= 0 {
652 Some(diff)
653 } else {
654 None
655 }
656 }
657
658 pub fn add_nanos(&self, nanos: i64) -> NanoTimestamp {
660 NanoTimestamp(self.0 + nanos)
661 }
662
663 pub fn add_millis(&self, ms: i64) -> NanoTimestamp {
665 NanoTimestamp(self.0 + ms * 1_000_000)
666 }
667
668 pub fn add_seconds(&self, secs: i64) -> NanoTimestamp {
670 NanoTimestamp(self.0 + secs * 1_000_000_000)
671 }
672
673 pub fn add_minutes(&self, minutes: i64) -> NanoTimestamp {
675 NanoTimestamp(self.0 + minutes * 60_000_000_000)
676 }
677
678 pub fn add_hours(&self, hours: i64) -> NanoTimestamp {
680 NanoTimestamp(self.0 + hours * 3_600_000_000_000)
681 }
682
683 pub fn is_before(&self, other: NanoTimestamp) -> bool {
685 self.0 < other.0
686 }
687
688 pub fn is_after(&self, other: NanoTimestamp) -> bool {
690 self.0 > other.0
691 }
692
693 pub fn is_same_second(&self, other: NanoTimestamp) -> bool {
698 self.0.div_euclid(1_000_000_000) == other.0.div_euclid(1_000_000_000)
699 }
700
701 pub fn is_same_minute(&self, other: NanoTimestamp) -> bool {
706 self.0.div_euclid(60_000_000_000) == other.0.div_euclid(60_000_000_000)
707 }
708
709 pub fn from_millis(ms: i64) -> Self {
711 Self(ms * 1_000_000)
712 }
713
714 pub fn to_millis(&self) -> i64 {
716 self.0 / 1_000_000
717 }
718
719 pub fn from_secs(secs: i64) -> Self {
721 Self(secs * 1_000_000_000)
722 }
723
724 pub fn to_secs(&self) -> i64 {
726 self.0 / 1_000_000_000
727 }
728
729 pub fn from_datetime(dt: DateTime<Utc>) -> Self {
733 Self(dt.timestamp_nanos_opt().unwrap_or(0))
734 }
735
736 pub fn to_datetime(&self) -> DateTime<Utc> {
738 let secs = self.0 / 1_000_000_000;
739 #[allow(clippy::cast_sign_loss)]
740 let nanos = (self.0 % 1_000_000_000) as u32;
741 Utc.timestamp_opt(secs, nanos).single().unwrap_or_else(|| {
742 Utc.timestamp_opt(0, 0)
743 .single()
744 .unwrap_or(DateTime::<Utc>::MIN_UTC)
745 })
746 }
747
748 pub fn to_seconds(&self) -> f64 {
750 self.0 as f64 / 1_000_000_000.0
751 }
752
753 pub fn duration_millis(self, other: NanoTimestamp) -> i64 {
757 (self.0 - other.0) / 1_000_000
758 }
759
760 pub fn min(self, other: NanoTimestamp) -> NanoTimestamp {
762 if self.0 <= other.0 { self } else { other }
763 }
764
765 pub fn max(self, other: NanoTimestamp) -> NanoTimestamp {
767 if self.0 >= other.0 { self } else { other }
768 }
769
770 pub fn elapsed_since(self, earlier: NanoTimestamp) -> i64 {
775 self.0 - earlier.0
776 }
777
778 pub fn seconds_since(self, earlier: NanoTimestamp) -> i64 {
782 (self.0 - earlier.0) / 1_000_000_000
783 }
784
785 pub fn minutes_since(self, earlier: NanoTimestamp) -> i64 {
789 (self.0 - earlier.0) / 60_000_000_000
790 }
791
792 pub fn hours_since(self, earlier: NanoTimestamp) -> i64 {
796 (self.0 - earlier.0) / 3_600_000_000_000
797 }
798
799 pub fn round_down_to(&self, period_nanos: i64) -> NanoTimestamp {
806 if period_nanos == 0 {
807 return *self;
808 }
809 NanoTimestamp(self.0 - self.0.rem_euclid(period_nanos))
810 }
811
812 pub fn to_date_string(&self) -> String {
816 use chrono::{DateTime, Utc};
817 let secs = self.0 / 1_000_000_000;
818 let nanos_part = (self.0 % 1_000_000_000).unsigned_abs() as u32;
819 let dt = DateTime::<Utc>::from_timestamp(secs, nanos_part)
820 .unwrap_or_default();
821 dt.format("%Y-%m-%d").to_string()
822 }
823
824 pub fn is_same_day(&self, other: NanoTimestamp) -> bool {
828 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
830 self.0.div_euclid(DAY_NANOS) == other.0.div_euclid(DAY_NANOS)
831 }
832
833 pub fn floor_to_hour(&self) -> NanoTimestamp {
839 const HOUR_NANOS: i64 = 3_600 * 1_000_000_000;
840 NanoTimestamp(self.0.div_euclid(HOUR_NANOS) * HOUR_NANOS)
841 }
842
843 pub fn hour_of_day(self) -> u8 {
845 use chrono::Timelike;
846 self.to_datetime().hour() as u8
847 }
848
849 pub fn minute_of_hour(self) -> u8 {
851 use chrono::Timelike;
852 self.to_datetime().minute() as u8
853 }
854
855 pub fn is_market_hours(self, open_hour: u8, close_hour: u8) -> bool {
861 if open_hour >= close_hour { return false; }
862 let h = self.hour_of_day();
863 h >= open_hour && h < close_hour
864 }
865
866 pub fn floor_to_day(&self) -> NanoTimestamp {
870 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
871 NanoTimestamp(self.0.div_euclid(DAY_NANOS) * DAY_NANOS)
872 }
873
874 pub fn floor_to_minute(&self) -> NanoTimestamp {
876 const MINUTE_NANOS: i64 = 60 * 1_000_000_000;
877 NanoTimestamp(self.0.div_euclid(MINUTE_NANOS) * MINUTE_NANOS)
878 }
879
880 pub fn elapsed_seconds(&self, other: NanoTimestamp) -> f64 {
884 (self.0 - other.0) as f64 / 1_000_000_000.0
885 }
886
887 pub fn to_datetime_string(&self) -> String {
891 use chrono::{DateTime, Utc};
892 let secs = self.0 / 1_000_000_000;
893 let nanos_part = (self.0 % 1_000_000_000).unsigned_abs() as u32;
894 let dt = DateTime::<Utc>::from_timestamp(secs, nanos_part)
895 .unwrap_or_default();
896 dt.format("%Y-%m-%d %H:%M:%S").to_string()
897 }
898
899 pub fn is_between(self, start: NanoTimestamp, end: NanoTimestamp) -> bool {
901 self.0 >= start.0 && self.0 <= end.0
902 }
903
904 pub fn to_unix_ms(self) -> i64 {
906 self.0 / 1_000_000
907 }
908
909 pub fn to_unix_seconds(self) -> i64 {
911 self.0 / 1_000_000_000
912 }
913
914 pub fn second_of_minute(self) -> u8 {
916 use chrono::Timelike;
917 self.to_datetime().second() as u8
918 }
919
920 pub fn day_of_week(self) -> u8 {
924 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
925 let days = self.0.div_euclid(DAY_NANOS);
926 ((days + 3).rem_euclid(7)) as u8
928 }
929
930 pub fn sub_minutes(&self, minutes: i64) -> NanoTimestamp {
934 NanoTimestamp(self.0 - minutes * 60_000_000_000)
935 }
936
937 pub fn is_weekend(self) -> bool {
939 let dow = self.day_of_week();
940 dow == 5 || dow == 6
941 }
942
943 pub fn start_of_week(self) -> NanoTimestamp {
945 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
946 let dow = self.day_of_week() as i64; NanoTimestamp(self.floor_to_day().0 - dow * DAY_NANOS)
948 }
949
950 pub fn add_days(&self, days: i64) -> NanoTimestamp {
952 const DAY_NANOS: i64 = 86_400 * 1_000_000_000;
953 NanoTimestamp(self.0 + days * DAY_NANOS)
954 }
955
956 pub fn minutes_between(self, other: NanoTimestamp) -> u64 {
958 const MINUTE_NANOS: u64 = 60 * 1_000_000_000;
959 (self.0 - other.0).unsigned_abs() / MINUTE_NANOS
960 }
961
962 pub fn seconds_between(self, other: NanoTimestamp) -> u64 {
964 const SECOND_NANOS: u64 = 1_000_000_000;
965 (self.0 - other.0).unsigned_abs() / SECOND_NANOS
966 }
967
968 pub fn day_of_year(self) -> u16 {
972 use chrono::Datelike;
973 self.to_datetime().ordinal() as u16
974 }
975
976 pub fn quarter(self) -> u8 {
980 use chrono::Datelike;
981 let month = self.to_datetime().month();
982 ((month - 1) / 3 + 1) as u8
983 }
984
985 pub fn week_of_year(self) -> u32 {
989 use chrono::Datelike;
990 self.to_datetime().iso_week().week()
991 }
992
993 pub fn is_same_week(self, other: NanoTimestamp) -> bool {
995 use chrono::Datelike;
996 let a = self.to_datetime().iso_week();
997 let b = other.to_datetime().iso_week();
998 a.week() == b.week() && a.year() == b.year()
999 }
1000
1001 pub fn is_same_month(self, other: NanoTimestamp) -> bool {
1003 use chrono::Datelike;
1004 let a = self.to_datetime();
1005 let b = other.to_datetime();
1006 a.year() == b.year() && a.month() == b.month()
1007 }
1008
1009 pub fn floor_to_week(self) -> NanoTimestamp {
1011 use chrono::{Datelike, Duration};
1012 let dt = self.to_datetime();
1013 let days_since_monday = i64::from(dt.weekday().num_days_from_monday());
1014 let monday = dt.date_naive() - Duration::days(days_since_monday);
1015 NanoTimestamp::from_datetime(monday.and_time(chrono::NaiveTime::MIN).and_utc())
1016 }
1017
1018 pub fn is_same_year(self, other: NanoTimestamp) -> bool {
1020 use chrono::Datelike;
1021 self.to_datetime().year() == other.to_datetime().year()
1022 }
1023
1024 pub fn days_between(self, other: NanoTimestamp) -> u64 {
1026 let diff_nanos = (self.0 - other.0).unsigned_abs();
1027 diff_nanos / 86_400_000_000_000
1028 }
1029
1030 pub fn end_of_day(self) -> NanoTimestamp {
1032 use chrono::{Datelike, TimeZone, Timelike};
1033 let dt = chrono::Utc.timestamp_nanos(self.0);
1034 let eod = chrono::Utc
1035 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 23, 59, 59)
1036 .single()
1037 .map(|d| d.with_nanosecond(999_999_999).unwrap_or(d))
1038 .unwrap_or(dt);
1039 NanoTimestamp(eod.timestamp_nanos_opt().unwrap_or(self.0))
1040 }
1041
1042 pub fn start_of_month(self) -> NanoTimestamp {
1044 use chrono::{Datelike, TimeZone};
1045 let dt = chrono::Utc.timestamp_nanos(self.0);
1046 let som = chrono::Utc
1047 .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
1048 .single()
1049 .unwrap_or(dt);
1050 NanoTimestamp(som.timestamp_nanos_opt().unwrap_or(self.0))
1051 }
1052
1053 pub fn end_of_month(self) -> NanoTimestamp {
1055 use chrono::{Datelike, TimeZone};
1056 let dt = chrono::Utc.timestamp_nanos(self.0);
1057 let (next_year, next_month) = if dt.month() == 12 {
1059 (dt.year() + 1, 1u32)
1060 } else {
1061 (dt.year(), dt.month() + 1)
1062 };
1063 let start_of_next = chrono::Utc
1064 .with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
1065 .single()
1066 .unwrap_or(dt);
1067 let nanos = start_of_next.timestamp_nanos_opt().unwrap_or(self.0) - 1;
1068 NanoTimestamp(nanos)
1069 }
1070
1071 pub fn floor_to_second(self) -> NanoTimestamp {
1073 const NANOS_PER_SECOND: i64 = 1_000_000_000;
1074 NanoTimestamp((self.0 / NANOS_PER_SECOND) * NANOS_PER_SECOND)
1075 }
1076
1077 pub fn is_same_hour(self, other: NanoTimestamp) -> bool {
1079 use chrono::{Datelike, TimeZone, Timelike};
1080 let a = chrono::Utc.timestamp_nanos(self.0);
1081 let b = chrono::Utc.timestamp_nanos(other.0);
1082 a.year() == b.year() && a.month() == b.month() && a.day() == b.day() && a.hour() == b.hour()
1083 }
1084
1085 pub fn add_weeks(&self, weeks: i64) -> NanoTimestamp {
1087 const NANOS_PER_WEEK: i64 = 7 * 24 * 3_600 * 1_000_000_000;
1088 NanoTimestamp(self.0 + weeks * NANOS_PER_WEEK)
1089 }
1090
1091 pub fn sub_hours(&self, hours: i64) -> NanoTimestamp {
1093 const NANOS_PER_HOUR: i64 = 3_600 * 1_000_000_000;
1094 NanoTimestamp(self.0 - hours * NANOS_PER_HOUR)
1095 }
1096
1097 pub fn sub_weeks(&self, weeks: i64) -> NanoTimestamp {
1099 const NANOS_PER_WEEK: i64 = 7 * 24 * 3_600 * 1_000_000_000;
1100 NanoTimestamp(self.0 - weeks * NANOS_PER_WEEK)
1101 }
1102
1103 pub fn sub_seconds(&self, secs: i64) -> NanoTimestamp {
1105 const NANOS_PER_SECOND: i64 = 1_000_000_000;
1106 NanoTimestamp(self.0 - secs * NANOS_PER_SECOND)
1107 }
1108
1109 pub fn to_time_string(&self) -> String {
1111 use chrono::{TimeZone, Timelike};
1112 let dt = chrono::Utc.timestamp_nanos(self.0);
1113 format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
1114 }
1115
1116 pub fn elapsed_hours(&self, other: NanoTimestamp) -> f64 {
1118 let diff = (self.0 - other.0).unsigned_abs();
1119 diff as f64 / (3_600.0 * 1_000_000_000.0)
1120 }
1121
1122 pub fn is_today(&self, other: NanoTimestamp) -> bool {
1124 self.is_same_day(other)
1125 }
1126
1127 pub fn nanoseconds_between(self, other: NanoTimestamp) -> u64 {
1129 (self.0 - other.0).unsigned_abs()
1130 }
1131
1132 pub fn elapsed_minutes(&self, other: NanoTimestamp) -> f64 {
1134 let diff = (self.0 - other.0).unsigned_abs();
1135 diff as f64 / (60.0 * 1_000_000_000.0)
1136 }
1137
1138 pub fn elapsed_days(&self, other: NanoTimestamp) -> f64 {
1140 let diff = (self.0 - other.0).unsigned_abs();
1141 diff as f64 / (86_400.0 * 1_000_000_000.0)
1142 }
1143
1144 pub fn sub_nanos(&self, nanos: i64) -> NanoTimestamp {
1146 NanoTimestamp(self.0 - nanos)
1147 }
1148
1149 pub fn start_of_year(self) -> NanoTimestamp {
1151 use chrono::{Datelike, TimeZone};
1152 let dt = chrono::Utc.timestamp_nanos(self.0);
1153 let start = chrono::Utc
1154 .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
1155 .single()
1156 .unwrap_or(dt);
1157 NanoTimestamp(start.timestamp_nanos_opt().unwrap_or(self.0))
1158 }
1159
1160 pub fn end_of_year(self) -> NanoTimestamp {
1162 use chrono::{Datelike, TimeZone};
1163 let dt = chrono::Utc.timestamp_nanos(self.0);
1164 let start_next = chrono::Utc
1165 .with_ymd_and_hms(dt.year() + 1, 1, 1, 0, 0, 0)
1166 .single()
1167 .unwrap_or(dt);
1168 let nanos = start_next.timestamp_nanos_opt().unwrap_or(self.0) - 1;
1169 NanoTimestamp(nanos)
1170 }
1171
1172 pub fn add_months(&self, months: i32) -> NanoTimestamp {
1174 use chrono::{Datelike, TimeZone};
1175 let dt = chrono::Utc.timestamp_nanos(self.0);
1176 let total_months = dt.month() as i32 + months;
1177 let year = dt.year() + (total_months - 1).div_euclid(12);
1178 let month = ((total_months - 1).rem_euclid(12) + 1) as u32;
1179 let day = dt.day().min(days_in_month(year, month));
1180 let new_dt = chrono::Utc
1181 .with_ymd_and_hms(year, month, day, dt.hour(), dt.minute(), dt.second())
1182 .single()
1183 .unwrap_or(dt);
1184 NanoTimestamp(new_dt.timestamp_nanos_opt().unwrap_or(self.0))
1185 }
1186
1187 pub fn start_of_quarter(self) -> NanoTimestamp {
1190 use chrono::{Datelike, TimeZone};
1191 let dt = chrono::Utc.timestamp_nanos(self.0);
1192 let quarter_start_month = ((dt.month() - 1) / 3) * 3 + 1;
1193 chrono::Utc
1194 .with_ymd_and_hms(dt.year(), quarter_start_month, 1, 0, 0, 0)
1195 .single()
1196 .map(|d| NanoTimestamp(d.timestamp_nanos_opt().unwrap_or(self.0)))
1197 .unwrap_or(self)
1198 }
1199
1200 pub fn end_of_quarter(self) -> NanoTimestamp {
1202 use chrono::{Datelike, TimeZone};
1203 let dt = chrono::Utc.timestamp_nanos(self.0);
1204 let quarter_end_month = ((dt.month() - 1) / 3) * 3 + 3;
1205 let last_day = days_in_month(dt.year(), quarter_end_month);
1206 chrono::Utc
1207 .with_ymd_and_hms(dt.year(), quarter_end_month, last_day, 23, 59, 59)
1208 .single()
1209 .map(|d| NanoTimestamp(d.timestamp_nanos_opt().unwrap_or(self.0) + 999_999_999))
1210 .unwrap_or(self)
1211 }
1212
1213 pub fn is_same_quarter(self, other: NanoTimestamp) -> bool {
1215 use chrono::{Datelike, TimeZone};
1216 let a = chrono::Utc.timestamp_nanos(self.0);
1217 let b = chrono::Utc.timestamp_nanos(other.0);
1218 a.year() == b.year() && ((a.month() - 1) / 3) == ((b.month() - 1) / 3)
1219 }
1220}
1221
1222fn days_in_month(year: i32, month: u32) -> u32 {
1223 match month {
1224 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1225 4 | 6 | 9 | 11 => 30,
1226 2 if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) => 29,
1227 2 => 28,
1228 _ => 30,
1229 }
1230}
1231
1232impl std::ops::Add<i64> for NanoTimestamp {
1234 type Output = NanoTimestamp;
1235 fn add(self, rhs: i64) -> NanoTimestamp {
1236 NanoTimestamp(self.0 + rhs)
1237 }
1238}
1239
1240impl std::ops::Sub<i64> for NanoTimestamp {
1242 type Output = NanoTimestamp;
1243 fn sub(self, rhs: i64) -> NanoTimestamp {
1244 NanoTimestamp(self.0 - rhs)
1245 }
1246}
1247
1248impl std::ops::Sub<NanoTimestamp> for NanoTimestamp {
1250 type Output = i64;
1251 fn sub(self, rhs: NanoTimestamp) -> i64 {
1252 self.0 - rhs.0
1253 }
1254}
1255
1256impl std::fmt::Display for NanoTimestamp {
1257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1258 write!(f, "{}", self.0)
1259 }
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use super::*;
1265 use rust_decimal_macros::dec;
1266
1267 #[test]
1270 fn test_symbol_new_valid_ok() {
1271 let sym = Symbol::new("AAPL").unwrap();
1272 assert_eq!(sym.as_str(), "AAPL");
1273 }
1274
1275 #[test]
1276 fn test_symbol_new_empty_fails() {
1277 let result = Symbol::new("");
1278 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1279 }
1280
1281 #[test]
1282 fn test_symbol_new_whitespace_fails() {
1283 let result = Symbol::new("AA PL");
1284 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1285 }
1286
1287 #[test]
1288 fn test_symbol_new_leading_whitespace_fails() {
1289 let result = Symbol::new(" AAPL");
1290 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
1291 }
1292
1293 #[test]
1294 fn test_symbol_display() {
1295 let sym = Symbol::new("TSLA").unwrap();
1296 assert_eq!(format!("{sym}"), "TSLA");
1297 }
1298
1299 #[test]
1300 fn test_symbol_clone_equality() {
1301 let a = Symbol::new("BTC").unwrap();
1302 let b = a.clone();
1303 assert_eq!(a, b);
1304 }
1305
1306 #[test]
1307 fn test_symbol_arc_clone_is_cheap() {
1308 let a = Symbol::new("ETH").unwrap();
1309 let b = a.clone();
1310 assert_eq!(a.as_str().as_ptr(), b.as_str().as_ptr());
1311 }
1312
1313 #[test]
1316 fn test_price_new_positive_ok() {
1317 let p = Price::new(dec!(100.5)).unwrap();
1318 assert_eq!(p.value(), dec!(100.5));
1319 }
1320
1321 #[test]
1322 fn test_price_new_zero_fails() {
1323 let result = Price::new(dec!(0));
1324 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
1325 }
1326
1327 #[test]
1328 fn test_price_new_negative_fails() {
1329 let result = Price::new(dec!(-1));
1330 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
1331 }
1332
1333 #[test]
1334 fn test_price_ordering() {
1335 let p1 = Price::new(dec!(1)).unwrap();
1336 let p2 = Price::new(dec!(2)).unwrap();
1337 assert!(p1 < p2);
1338 }
1339
1340 #[test]
1341 fn test_price_add() {
1342 let a = Price::new(dec!(10)).unwrap();
1343 let b = Price::new(dec!(5)).unwrap();
1344 assert_eq!(a + b, dec!(15));
1345 }
1346
1347 #[test]
1348 fn test_price_sub() {
1349 let a = Price::new(dec!(10)).unwrap();
1350 let b = Price::new(dec!(3)).unwrap();
1351 assert_eq!(a - b, dec!(7));
1352 }
1353
1354 #[test]
1355 fn test_price_mul_quantity() {
1356 let p = Price::new(dec!(10)).unwrap();
1357 let q = Quantity::new(dec!(5)).unwrap();
1358 assert_eq!(p * q, dec!(50));
1359 }
1360
1361 #[test]
1362 fn test_price_mul_decimal_valid() {
1363 let p = Price::new(dec!(10)).unwrap();
1364 assert_eq!((p * dec!(2)).unwrap().value(), dec!(20));
1365 }
1366
1367 #[test]
1368 fn test_price_mul_decimal_zero_returns_none() {
1369 let p = Price::new(dec!(10)).unwrap();
1370 assert!((p * dec!(0)).is_none());
1371 }
1372
1373 #[test]
1376 fn test_quantity_new_zero_ok() {
1377 let q = Quantity::new(dec!(0)).unwrap();
1378 assert_eq!(q.value(), dec!(0));
1379 }
1380
1381 #[test]
1382 fn test_quantity_new_positive_ok() {
1383 let q = Quantity::new(dec!(5.5)).unwrap();
1384 assert_eq!(q.value(), dec!(5.5));
1385 }
1386
1387 #[test]
1388 fn test_quantity_new_negative_fails() {
1389 let result = Quantity::new(dec!(-0.01));
1390 assert!(matches!(result, Err(FinError::InvalidQuantity(_))));
1391 }
1392
1393 #[test]
1394 fn test_quantity_zero_constructor() {
1395 let q = Quantity::zero();
1396 assert_eq!(q.value(), Decimal::ZERO);
1397 }
1398
1399 #[test]
1400 fn test_quantity_add() {
1401 let a = Quantity::new(dec!(3)).unwrap();
1402 let b = Quantity::new(dec!(4)).unwrap();
1403 assert_eq!((a + b).value(), dec!(7));
1404 }
1405
1406 #[test]
1407 fn test_quantity_sub_positive() {
1408 let a = Quantity::new(dec!(10)).unwrap();
1409 let b = Quantity::new(dec!(3)).unwrap();
1410 assert_eq!(a - b, dec!(7));
1411 }
1412
1413 #[test]
1414 fn test_quantity_sub_negative() {
1415 let a = Quantity::new(dec!(3)).unwrap();
1416 let b = Quantity::new(dec!(10)).unwrap();
1417 assert_eq!(a - b, dec!(-7));
1418 }
1419
1420 #[test]
1421 fn test_quantity_mul_decimal() {
1422 let q = Quantity::new(dec!(5)).unwrap();
1423 assert_eq!(q * dec!(3), dec!(15));
1424 }
1425
1426 #[test]
1427 fn test_quantity_is_zero() {
1428 assert!(Quantity::zero().is_zero());
1429 assert!(!Quantity::new(dec!(1)).unwrap().is_zero());
1430 }
1431
1432 #[test]
1435 fn test_side_display_bid() {
1436 assert_eq!(format!("{}", Side::Bid), "Bid");
1437 }
1438
1439 #[test]
1440 fn test_side_display_ask() {
1441 assert_eq!(format!("{}", Side::Ask), "Ask");
1442 }
1443
1444 #[test]
1447 fn test_nano_timestamp_now_positive() {
1448 let ts = NanoTimestamp::now();
1449 assert!(ts.nanos() > 0);
1450 }
1451
1452 #[test]
1453 fn test_nano_timestamp_ordering() {
1454 let ts1 = NanoTimestamp::new(1_000_000_000);
1455 let ts2 = NanoTimestamp::new(2_000_000_000);
1456 assert!(ts1 < ts2);
1457 }
1458
1459 #[test]
1460 fn test_nano_timestamp_to_datetime_epoch() {
1461 let ts = NanoTimestamp::new(0);
1462 let dt = ts.to_datetime();
1463 assert_eq!(dt.timestamp(), 0);
1464 }
1465
1466 #[test]
1467 fn test_nano_timestamp_to_datetime_roundtrip() {
1468 let ts = NanoTimestamp::new(1_700_000_000_000_000_000_i64);
1469 let dt = ts.to_datetime();
1470 assert_eq!(
1471 dt.timestamp_nanos_opt().unwrap_or(0),
1472 1_700_000_000_000_000_000_i64
1473 );
1474 }
1475
1476 #[test]
1477 fn test_nano_timestamp_nanos_roundtrip() {
1478 let ts = NanoTimestamp::new(42_000_000);
1479 assert_eq!(ts.nanos(), 42_000_000);
1480 }
1481
1482 #[test]
1483 fn test_nano_timestamp_duration_since_positive() {
1484 let a = NanoTimestamp::new(1_000);
1485 let b = NanoTimestamp::new(600);
1486 assert_eq!(a.duration_since(b), 400);
1487 }
1488
1489 #[test]
1490 fn test_nano_timestamp_duration_since_negative() {
1491 let a = NanoTimestamp::new(500);
1492 let b = NanoTimestamp::new(1_000);
1493 assert_eq!(a.duration_since(b), -500);
1494 }
1495
1496 #[test]
1497 fn test_symbol_len() {
1498 let sym = Symbol::new("AAPL").unwrap();
1499 assert_eq!(sym.len(), 4);
1500 }
1501
1502 #[test]
1503 fn test_symbol_is_empty_always_false() {
1504 let sym = Symbol::new("X").unwrap();
1505 assert!(!sym.is_empty());
1506 }
1507
1508 #[test]
1509 fn test_symbol_try_from_string_valid() {
1510 let sym = Symbol::try_from("AAPL".to_owned()).unwrap();
1511 assert_eq!(sym.as_str(), "AAPL");
1512 }
1513
1514 #[test]
1515 fn test_symbol_try_from_str_valid() {
1516 let sym = Symbol::try_from("ETH").unwrap();
1517 assert_eq!(sym.as_str(), "ETH");
1518 }
1519
1520 #[test]
1521 fn test_symbol_try_from_empty_fails() {
1522 assert!(Symbol::try_from("").is_err());
1523 }
1524
1525 #[test]
1526 fn test_symbol_try_from_whitespace_fails() {
1527 assert!(Symbol::try_from("BTC USD").is_err());
1528 }
1529
1530 #[test]
1531 fn test_nano_timestamp_from_datetime_roundtrip() {
1532 let original = NanoTimestamp::new(1_700_000_000_000_000_000_i64);
1533 let dt = original.to_datetime();
1534 let recovered = NanoTimestamp::from_datetime(dt);
1535 assert_eq!(recovered.nanos(), original.nanos());
1536 }
1537
1538 #[test]
1539 fn test_nano_timestamp_from_datetime_epoch() {
1540 use chrono::Utc;
1541 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
1542 let ts = NanoTimestamp::from_datetime(epoch);
1543 assert_eq!(ts.nanos(), 0);
1544 }
1545
1546 #[test]
1547 fn test_price_to_f64() {
1548 let p = Price::new(dec!(123.45)).unwrap();
1549 let f = p.to_f64();
1550 assert!((f - 123.45_f64).abs() < 1e-6);
1551 }
1552
1553 #[test]
1554 fn test_quantity_to_f64() {
1555 let q = Quantity::new(dec!(42)).unwrap();
1556 assert!((q.to_f64() - 42.0_f64).abs() < 1e-10);
1557 }
1558
1559 #[test]
1560 fn test_price_from_f64_valid() {
1561 let p = Price::from_f64(42.5).unwrap();
1562 assert!((p.to_f64() - 42.5).abs() < 1e-6);
1563 }
1564
1565 #[test]
1566 fn test_price_from_f64_zero_returns_none() {
1567 assert!(Price::from_f64(0.0).is_none());
1568 }
1569
1570 #[test]
1571 fn test_price_from_f64_negative_returns_none() {
1572 assert!(Price::from_f64(-1.0).is_none());
1573 }
1574
1575 #[test]
1576 fn test_quantity_from_f64_valid() {
1577 let q = Quantity::from_f64(10.0).unwrap();
1578 assert!((q.to_f64() - 10.0).abs() < 1e-10);
1579 }
1580
1581 #[test]
1582 fn test_quantity_from_f64_zero_valid() {
1583 let q = Quantity::from_f64(0.0).unwrap();
1584 assert!(q.is_zero());
1585 }
1586
1587 #[test]
1588 fn test_quantity_from_f64_negative_returns_none() {
1589 assert!(Quantity::from_f64(-1.0).is_none());
1590 }
1591
1592 #[test]
1593 fn test_nano_timestamp_add_millis() {
1594 let ts = NanoTimestamp::new(0);
1595 assert_eq!(ts.add_millis(1).nanos(), 1_000_000);
1596 }
1597
1598 #[test]
1599 fn test_nano_timestamp_add_seconds() {
1600 let ts = NanoTimestamp::new(0);
1601 assert_eq!(ts.add_seconds(2).nanos(), 2_000_000_000);
1602 }
1603
1604 #[test]
1605 fn test_nano_timestamp_is_before_after() {
1606 let a = NanoTimestamp::new(1_000);
1607 let b = NanoTimestamp::new(2_000);
1608 assert!(a.is_before(b));
1609 assert!(b.is_after(a));
1610 assert!(!a.is_after(b));
1611 assert!(!b.is_before(a));
1612 }
1613
1614 #[test]
1615 fn test_nano_timestamp_from_secs_roundtrip() {
1616 let ts = NanoTimestamp::from_secs(1_700_000_000);
1617 assert_eq!(ts.to_secs(), 1_700_000_000);
1618 }
1619
1620 #[test]
1621 fn test_nano_timestamp_from_secs_truncates_sub_second() {
1622 let ts = NanoTimestamp::new(1_700_000_000_999_999_999);
1623 assert_eq!(ts.to_secs(), 1_700_000_000);
1624 }
1625
1626 #[test]
1627 fn test_symbol_ord_lexicographic() {
1628 let a = Symbol::new("AAPL").unwrap();
1629 let b = Symbol::new("MSFT").unwrap();
1630 let c = Symbol::new("AAPL").unwrap();
1631 assert!(a < b);
1632 assert!(b > a);
1633 assert_eq!(a.cmp(&c), std::cmp::Ordering::Equal);
1634 }
1635
1636 #[test]
1637 fn test_symbol_ord_usable_in_btreemap() {
1638 use std::collections::BTreeMap;
1639 let mut m: BTreeMap<Symbol, i32> = BTreeMap::new();
1640 m.insert(Symbol::new("Z").unwrap(), 3);
1641 m.insert(Symbol::new("A").unwrap(), 1);
1642 m.insert(Symbol::new("M").unwrap(), 2);
1643 let keys: Vec<_> = m.keys().map(|s| s.as_str()).collect();
1644 assert_eq!(keys, ["A", "M", "Z"]);
1645 }
1646
1647 #[test]
1648 fn test_price_pct_change_positive() {
1649 let p1 = Price::new(dec!(100)).unwrap();
1650 let p2 = Price::new(dec!(110)).unwrap();
1651 assert_eq!(p1.pct_change_to(p2), dec!(10));
1652 }
1653
1654 #[test]
1655 fn test_price_pct_change_negative() {
1656 let p1 = Price::new(dec!(100)).unwrap();
1657 let p2 = Price::new(dec!(90)).unwrap();
1658 assert_eq!(p1.pct_change_to(p2), dec!(-10));
1659 }
1660
1661 #[test]
1662 fn test_price_pct_change_zero() {
1663 let p = Price::new(dec!(100)).unwrap();
1664 assert_eq!(p.pct_change_to(p), dec!(0));
1665 }
1666
1667 #[test]
1668 fn test_nano_timestamp_elapsed_is_non_negative_for_past() {
1669 let past = NanoTimestamp::new(0); assert!(past.elapsed() > 0);
1671 }
1672
1673 #[test]
1674 fn test_price_checked_mul_some() {
1675 let p = Price::new(dec!(100)).unwrap();
1676 let q = Quantity::new(dec!(5)).unwrap();
1677 assert_eq!(p.checked_mul(q), Some(dec!(500)));
1678 }
1679
1680 #[test]
1681 fn test_price_checked_mul_with_zero_qty() {
1682 let p = Price::new(dec!(100)).unwrap();
1683 let q = Quantity::zero();
1684 assert_eq!(p.checked_mul(q), Some(dec!(0)));
1685 }
1686
1687 #[test]
1688 fn test_quantity_checked_add() {
1689 let a = Quantity::new(dec!(10)).unwrap();
1690 let b = Quantity::new(dec!(5)).unwrap();
1691 assert_eq!(a.checked_add(b).map(|q| q.value()), Some(dec!(15)));
1692 }
1693
1694 #[test]
1695 fn test_nano_timestamp_min_less_than_max() {
1696 assert!(NanoTimestamp::MIN < NanoTimestamp::MAX);
1697 assert!(NanoTimestamp::MIN < NanoTimestamp::new(0));
1698 assert!(NanoTimestamp::new(0) < NanoTimestamp::MAX);
1699 }
1700
1701 #[test]
1702 fn test_price_midpoint() {
1703 let bid = Price::new(dec!(99)).unwrap();
1704 let ask = Price::new(dec!(101)).unwrap();
1705 assert_eq!(Price::midpoint(bid, ask), dec!(100));
1706 }
1707
1708 #[test]
1709 fn test_price_midpoint_same_price() {
1710 let p = Price::new(dec!(100)).unwrap();
1711 assert_eq!(Price::midpoint(p, p), dec!(100));
1712 }
1713
1714 #[test]
1715 fn test_price_mid_method() {
1716 let bid = Price::new(dec!(100)).unwrap();
1717 let ask = Price::new(dec!(102)).unwrap();
1718 let mid = bid.mid(ask);
1719 assert_eq!(mid.value(), dec!(101));
1720 }
1721
1722 #[test]
1723 fn test_price_mid_method_same_price() {
1724 let p = Price::new(dec!(100)).unwrap();
1725 assert_eq!(p.mid(p).value(), dec!(100));
1726 }
1727
1728 #[test]
1729 fn test_price_abs_diff_positive() {
1730 let a = Price::new(dec!(105)).unwrap();
1731 let b = Price::new(dec!(100)).unwrap();
1732 assert_eq!(a.abs_diff(b), dec!(5));
1733 assert_eq!(b.abs_diff(a), dec!(5));
1734 }
1735
1736 #[test]
1737 fn test_price_abs_diff_same() {
1738 let p = Price::new(dec!(100)).unwrap();
1739 assert_eq!(p.abs_diff(p), dec!(0));
1740 }
1741
1742 #[test]
1743 fn test_quantity_checked_sub_valid() {
1744 let a = Quantity::new(dec!(10)).unwrap();
1745 let b = Quantity::new(dec!(3)).unwrap();
1746 assert_eq!(a.checked_sub(b).unwrap().value(), dec!(7));
1747 }
1748
1749 #[test]
1750 fn test_quantity_checked_sub_exact_zero() {
1751 let a = Quantity::new(dec!(5)).unwrap();
1752 let b = Quantity::new(dec!(5)).unwrap();
1753 assert_eq!(a.checked_sub(b).unwrap().value(), dec!(0));
1754 }
1755
1756 #[test]
1757 fn test_quantity_checked_sub_negative_returns_none() {
1758 let a = Quantity::new(dec!(3)).unwrap();
1759 let b = Quantity::new(dec!(5)).unwrap();
1760 assert!(a.checked_sub(b).is_none());
1761 }
1762
1763 #[test]
1764 fn test_nano_timestamp_min_returns_earlier() {
1765 let t1 = NanoTimestamp::new(100);
1766 let t2 = NanoTimestamp::new(200);
1767 assert_eq!(t1.min(t2), t1);
1768 assert_eq!(t2.min(t1), t1);
1769 }
1770
1771 #[test]
1772 fn test_nano_timestamp_max_returns_later() {
1773 let t1 = NanoTimestamp::new(100);
1774 let t2 = NanoTimestamp::new(200);
1775 assert_eq!(t1.max(t2), t2);
1776 assert_eq!(t2.max(t1), t2);
1777 }
1778
1779 #[test]
1780 fn test_nano_timestamp_min_max_same() {
1781 let t = NanoTimestamp::new(500);
1782 assert_eq!(t.min(t), t);
1783 assert_eq!(t.max(t), t);
1784 }
1785
1786 #[test]
1787 fn test_side_opposite_bid() {
1788 assert_eq!(Side::Bid.opposite(), Side::Ask);
1789 }
1790
1791 #[test]
1792 fn test_side_opposite_ask() {
1793 assert_eq!(Side::Ask.opposite(), Side::Bid);
1794 }
1795
1796 #[test]
1797 fn test_side_opposite_involution() {
1798 assert_eq!(Side::Bid.opposite().opposite(), Side::Bid);
1799 }
1800
1801 #[test]
1802 fn test_price_checked_add_valid() {
1803 let a = Price::new(dec!(100)).unwrap();
1804 let b = Price::new(dec!(50)).unwrap();
1805 assert_eq!(a.checked_add(b).unwrap().value(), dec!(150));
1806 }
1807
1808 #[test]
1809 fn test_price_checked_add_result_validated() {
1810 let a = Price::new(dec!(1)).unwrap();
1812 let b = Price::new(dec!(2)).unwrap();
1813 assert!(a.checked_add(b).is_some());
1814 }
1815
1816 #[test]
1817 fn test_price_lerp_midpoint() {
1818 let a = Price::new(dec!(100)).unwrap();
1819 let b = Price::new(dec!(200)).unwrap();
1820 let mid = a.lerp(b, dec!(0.5)).unwrap();
1821 assert_eq!(mid.value(), dec!(150));
1822 }
1823
1824 #[test]
1825 fn test_price_lerp_at_zero_returns_self() {
1826 let a = Price::new(dec!(100)).unwrap();
1827 let b = Price::new(dec!(200)).unwrap();
1828 assert_eq!(a.lerp(b, Decimal::ZERO).unwrap().value(), dec!(100));
1829 }
1830
1831 #[test]
1832 fn test_price_lerp_at_one_returns_other() {
1833 let a = Price::new(dec!(100)).unwrap();
1834 let b = Price::new(dec!(200)).unwrap();
1835 assert_eq!(a.lerp(b, Decimal::ONE).unwrap().value(), dec!(200));
1836 }
1837
1838 #[test]
1839 fn test_price_lerp_out_of_range_returns_none() {
1840 let a = Price::new(dec!(100)).unwrap();
1841 let b = Price::new(dec!(200)).unwrap();
1842 assert!(a.lerp(b, dec!(1.5)).is_none());
1843 assert!(a.lerp(b, dec!(-0.1)).is_none());
1844 }
1845
1846 #[test]
1847 fn test_quantity_scale_half() {
1848 let q = Quantity::new(dec!(100)).unwrap();
1849 let result = q.scale(dec!(0.5)).unwrap();
1850 assert_eq!(result.value(), dec!(50));
1851 }
1852
1853 #[test]
1854 fn test_quantity_scale_zero_factor() {
1855 let q = Quantity::new(dec!(100)).unwrap();
1856 let result = q.scale(Decimal::ZERO).unwrap();
1857 assert_eq!(result.value(), dec!(0));
1858 }
1859
1860 #[test]
1861 fn test_quantity_scale_negative_factor_returns_none() {
1862 let q = Quantity::new(dec!(100)).unwrap();
1863 assert!(q.scale(dec!(-1)).is_none());
1864 }
1865
1866 #[test]
1867 fn test_nano_timestamp_elapsed_since_positive() {
1868 let earlier = NanoTimestamp::new(1000);
1869 let later = NanoTimestamp::new(3000);
1870 assert_eq!(later.elapsed_since(earlier), 2000);
1871 }
1872
1873 #[test]
1874 fn test_nano_timestamp_elapsed_since_negative() {
1875 let earlier = NanoTimestamp::new(1000);
1876 let later = NanoTimestamp::new(3000);
1877 assert_eq!(earlier.elapsed_since(later), -2000);
1879 }
1880
1881 #[test]
1882 fn test_nano_timestamp_elapsed_since_same_is_zero() {
1883 let ts = NanoTimestamp::new(5000);
1884 assert_eq!(ts.elapsed_since(ts), 0);
1885 }
1886
1887 #[test]
1888 fn test_nano_timestamp_to_seconds_one_second() {
1889 let ts = NanoTimestamp::new(1_000_000_000);
1890 assert!((ts.to_seconds() - 1.0_f64).abs() < 1e-9);
1891 }
1892
1893 #[test]
1894 fn test_nano_timestamp_to_seconds_zero() {
1895 let ts = NanoTimestamp::new(0);
1896 assert_eq!(ts.to_seconds(), 0.0);
1897 }
1898
1899 #[test]
1900 fn test_quantity_split_even() {
1901 let q = Quantity::new(dec!(10)).unwrap();
1902 let parts = q.split(5);
1903 assert_eq!(parts.len(), 5);
1904 let total: Decimal = parts.iter().map(|p| p.value()).sum();
1905 assert_eq!(total, dec!(10));
1906 }
1907
1908 #[test]
1909 fn test_quantity_split_remainder_goes_to_last() {
1910 let q = Quantity::new(dec!(10)).unwrap();
1911 let parts = q.split(3);
1912 assert_eq!(parts.len(), 3);
1913 let total: Decimal = parts.iter().map(|p| p.value()).sum();
1914 assert_eq!(total, dec!(10));
1915 }
1916
1917 #[test]
1918 fn test_quantity_split_zero_n_returns_empty() {
1919 let q = Quantity::new(dec!(10)).unwrap();
1920 assert!(q.split(0).is_empty());
1921 }
1922
1923 #[test]
1924 fn test_quantity_split_one_returns_self() {
1925 let q = Quantity::new(dec!(10)).unwrap();
1926 let parts = q.split(1);
1927 assert_eq!(parts.len(), 1);
1928 assert_eq!(parts[0].value(), dec!(10));
1929 }
1930
1931 #[test]
1932 fn test_price_pct_move_up() {
1933 let p = Price::new(dec!(100)).unwrap();
1934 let result = p.pct_move(dec!(10)).unwrap();
1935 assert_eq!(result.value(), dec!(110));
1936 }
1937
1938 #[test]
1939 fn test_price_pct_move_down() {
1940 let p = Price::new(dec!(100)).unwrap();
1941 let result = p.pct_move(dec!(-10)).unwrap();
1942 assert_eq!(result.value(), dec!(90));
1943 }
1944
1945 #[test]
1946 fn test_price_pct_move_negative_to_invalid() {
1947 let p = Price::new(dec!(100)).unwrap();
1948 assert!(p.pct_move(dec!(-100)).is_none());
1950 }
1951
1952 #[test]
1953 fn test_quantity_proportion_of_half() {
1954 let a = Quantity::new(dec!(5)).unwrap();
1955 let total = Quantity::new(dec!(10)).unwrap();
1956 assert_eq!(a.proportion_of(total), Some(dec!(0.5)));
1957 }
1958
1959 #[test]
1960 fn test_quantity_proportion_of_zero_total_returns_none() {
1961 let a = Quantity::new(dec!(5)).unwrap();
1962 let total = Quantity::zero();
1963 assert!(a.proportion_of(total).is_none());
1964 }
1965
1966 #[test]
1967 fn test_nano_timestamp_duration_millis() {
1968 let a = NanoTimestamp::new(0);
1969 let b = NanoTimestamp::new(1_500_000_000); assert_eq!(b.duration_millis(a), 1500);
1971 }
1972
1973 #[test]
1974 fn test_nano_timestamp_duration_millis_negative() {
1975 let a = NanoTimestamp::new(0);
1976 let b = NanoTimestamp::new(2_000_000_000);
1977 assert_eq!(a.duration_millis(b), -2000);
1978 }
1979
1980 #[test]
1981 fn test_nano_timestamp_minutes_since_positive() {
1982 let a = NanoTimestamp::new(0);
1983 let b = NanoTimestamp::new(3 * 60_000_000_000i64);
1984 assert_eq!(b.minutes_since(a), 3);
1985 }
1986
1987 #[test]
1988 fn test_nano_timestamp_minutes_since_negative() {
1989 let a = NanoTimestamp::new(0);
1990 let b = NanoTimestamp::new(3 * 60_000_000_000i64);
1991 assert_eq!(a.minutes_since(b), -3);
1992 }
1993
1994 #[test]
1995 fn test_nano_timestamp_hours_since_positive() {
1996 let a = NanoTimestamp::new(0);
1997 let b = NanoTimestamp::new(2 * 3_600_000_000_000i64);
1998 assert_eq!(b.hours_since(a), 2);
1999 }
2000
2001 #[test]
2002 fn test_nano_timestamp_hours_since_same_returns_zero() {
2003 let a = NanoTimestamp::new(1_000_000);
2004 assert_eq!(a.hours_since(a), 0);
2005 }
2006
2007 #[test]
2008 fn test_price_is_within_pct_same_price() {
2009 let p = Price::new(dec!(100)).unwrap();
2010 assert!(p.is_within_pct(p, dec!(0)));
2011 }
2012
2013 #[test]
2014 fn test_price_is_within_pct_within_range() {
2015 let p = Price::new(dec!(100)).unwrap();
2016 let q = Price::new(dec!(101)).unwrap();
2017 assert!(p.is_within_pct(q, dec!(2)));
2018 }
2019
2020 #[test]
2021 fn test_price_is_within_pct_outside_range() {
2022 let p = Price::new(dec!(100)).unwrap();
2023 let q = Price::new(dec!(110)).unwrap();
2024 assert!(!p.is_within_pct(q, dec!(5)));
2025 }
2026
2027 #[test]
2028 fn test_price_is_within_pct_negative_pct_returns_false() {
2029 let p = Price::new(dec!(100)).unwrap();
2030 assert!(!p.is_within_pct(p, dec!(-1)));
2031 }
2032
2033 #[test]
2034 fn test_timestamp_is_between_inclusive() {
2035 let ts = NanoTimestamp::new(500);
2036 assert!(ts.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2037 assert!(ts.is_between(NanoTimestamp::new(500), NanoTimestamp::new(500))); }
2039
2040 #[test]
2041 fn test_timestamp_is_between_outside() {
2042 let ts = NanoTimestamp::new(50);
2043 assert!(!ts.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2044 let ts2 = NanoTimestamp::new(1000);
2045 assert!(!ts2.is_between(NanoTimestamp::new(100), NanoTimestamp::new(900)));
2046 }
2047
2048 #[test]
2049 fn test_timestamp_to_unix_ms() {
2050 let ts = NanoTimestamp::new(1_500_000_000); assert_eq!(ts.to_unix_ms(), 1500);
2052 }
2053
2054 #[test]
2055 fn test_timestamp_to_unix_ms_truncates() {
2056 let ts = NanoTimestamp::new(1_999_999); assert_eq!(ts.to_unix_ms(), 1);
2058 }
2059
2060 #[test]
2061 fn test_price_round_to_tick_same_as_snap() {
2062 let p = Price::new(dec!(100.7)).unwrap();
2063 assert_eq!(p.round_to_tick(dec!(0.5)), p.snap_to_tick(dec!(0.5)));
2064 }
2065
2066 #[test]
2067 fn test_price_round_to_tick_invalid_tick_returns_none() {
2068 let p = Price::new(dec!(100)).unwrap();
2069 assert!(p.round_to_tick(dec!(0)).is_none());
2070 assert!(p.round_to_tick(dec!(-1)).is_none());
2071 }
2072
2073 #[test]
2074 fn test_nanotimestamp_day_of_week_epoch_is_thursday() {
2075 let ts = NanoTimestamp::new(0);
2077 assert_eq!(ts.day_of_week(), 3);
2078 }
2079
2080 #[test]
2081 fn test_nanotimestamp_day_of_week_next_day() {
2082 let ts = NanoTimestamp::new(86_400 * 1_000_000_000);
2084 assert_eq!(ts.day_of_week(), 4);
2085 }
2086
2087 #[test]
2088 fn test_nanotimestamp_sub_minutes_round_trip() {
2089 let ts = NanoTimestamp::new(3_600_000_000_000); let back = ts.sub_minutes(30);
2091 let forward = back.add_minutes(30);
2092 assert_eq!(forward.nanos(), ts.nanos());
2093 }
2094
2095 #[test]
2096 fn test_nanotimestamp_sub_minutes_by_zero() {
2097 let ts = NanoTimestamp::new(1_000_000_000);
2098 assert_eq!(ts.sub_minutes(0).nanos(), ts.nanos());
2099 }
2100}