Skip to main content

brokapi_alor/map/
mod.rs

1//! Mapping of ALOR wire models to the domain types of `brokapi-core`.
2
3pub 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
15/// The timestamps of an event: the time we received it and, if the broker gave one, the time of the broker.
16pub 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
30/// A time of the broker (UTC text such as `2020-06-16T23:59:59.9990000Z`).
31pub 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
38/// A date as [`UnixNanos`].
39pub fn unix_nanos(dt: DateTime<Utc>) -> UnixNanos {
40    UnixNanos::from_nanos(dt.timestamp_nanos_opt().unwrap_or(0))
41}
42
43/// Unix seconds as [`UnixNanos`].
44pub fn secs_to_nanos(secs: i64) -> UnixNanos {
45    UnixNanos::from_nanos(secs.saturating_mul(1_000_000_000))
46}
47
48/// Unix milliseconds as [`UnixNanos`].
49pub fn ms_to_nanos(ms: i64) -> UnixNanos {
50    UnixNanos::from_nanos(ms.saturating_mul(1_000_000))
51}
52
53/// The number of lots in a quantity of pieces: a positive whole number of lots, else `invalid_qty` (the library
54/// never rounds a quantity).
55pub 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
75/// Lots to pieces.
76pub 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}