1pub mod execution;
4pub mod instrument;
5pub mod market;
6pub mod order;
7pub mod portfolio;
8pub mod stop;
9
10use std::time::Instant;
11
12use brokapi_core::{Decimal, Error, Qty, Timestamps, UnixNanos};
13use chrono::{DateTime, Utc};
14
15pub fn timestamps(now: Instant, broker_ts: Option<UnixNanos>) -> Timestamps {
17 let rx_ts = UnixNanos::from_nanos(
18 std::time::SystemTime::now()
19 .duration_since(std::time::UNIX_EPOCH)
20 .map(|d| d.as_nanos() as i64)
21 .unwrap_or(0),
22 );
23 let mut ts = Timestamps::new(rx_ts, now);
24 if let Some(b) = broker_ts {
25 ts = ts.with_broker_ts(b);
26 }
27 ts
28}
29
30pub fn parse_time(raw: Option<&str>) -> Option<UnixNanos> {
32 let s = raw?;
33 DateTime::parse_from_rfc3339(s)
34 .ok()
35 .map(|dt| unix_nanos(dt.with_timezone(&Utc)))
36}
37
38pub fn unix_nanos(dt: DateTime<Utc>) -> UnixNanos {
40 UnixNanos::from_nanos(dt.timestamp_nanos_opt().unwrap_or(0))
41}
42
43pub fn secs_to_nanos(secs: i64) -> UnixNanos {
45 UnixNanos::from_nanos(secs.saturating_mul(1_000_000_000))
46}
47
48pub fn ms_to_nanos(ms: i64) -> UnixNanos {
50 UnixNanos::from_nanos(ms.saturating_mul(1_000_000))
51}
52
53pub fn pieces_to_lots(qty: Qty, lot: Decimal) -> Result<i64, Error> {
56 let bad = |why: String| Error::Rejected {
57 code: "invalid_qty".into(),
58 message: why,
59 };
60 if !lot.is_sign_positive() || lot.is_zero() {
61 return Err(bad(format!("the lot size {lot} is not usable")));
62 }
63 let lots = qty.decimal() / lot;
64 if lots.fract().is_zero() && lots >= Decimal::ONE {
65 use rust_decimal::prelude::ToPrimitive;
66 return lots
67 .to_i64()
68 .ok_or_else(|| bad(format!("quantity {qty} is too large")));
69 }
70 Err(bad(format!(
71 "quantity {qty} is not a positive whole number of lots of {lot}"
72 )))
73}
74
75pub fn lots_to_pieces(lots: Decimal, lot: Decimal) -> Qty {
77 Qty::new(lots * lot)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use brokapi_core::dec;
84
85 #[test]
86 fn pieces_become_whole_lots_or_are_refused() {
87 assert_eq!(pieces_to_lots(Qty::from(30), dec!(10)).unwrap(), 3);
88 assert_eq!(pieces_to_lots(Qty::from(1), dec!(1)).unwrap(), 1);
89 for q in ["0", "5", "15", "0.5"] {
90 assert!(
91 matches!(pieces_to_lots(q.parse().unwrap(), dec!(10)), Err(Error::Rejected { ref code, .. }) if code == "invalid_qty"),
92 "{q}"
93 );
94 }
95 assert!(pieces_to_lots(Qty::from(10), Decimal::ZERO).is_err());
96 }
97
98 #[test]
99 fn times_of_the_broker_are_read_with_seven_digits() {
100 let t = parse_time(Some("2020-06-16T23:59:59.9990000Z")).unwrap();
101 assert_eq!(t.as_nanos() % 1_000_000_000, 999_000_000);
102 assert!(parse_time(Some("nonsense")).is_none());
103 assert_eq!(secs_to_nanos(2).as_nanos(), 2_000_000_000);
104 assert_eq!(ms_to_nanos(2).as_nanos(), 2_000_000);
105 }
106}