Skip to main content

brokapi_bcs/map/
mod.rs

1//! Mappers from the BCS wire models to the brokapi-core domain types.
2//!
3//! The functions are pure: they take a model that the protocol crate parsed and the current monotonic
4//! time, and they return a domain value. They never fail loudly. A record that cannot be mapped
5//! safely is dropped (`None` or an empty list), not guessed. Where the documentation of the API leaves
6//! a choice open, the function says which choice it makes.
7//!
8//! The connectors call these functions. You need them only to read raw data from [`BcsRaw`](crate::BcsRaw)
9//! in the same shape as the shared traits give it.
10
11pub mod execution;
12pub mod instrument;
13pub mod market;
14pub mod order;
15pub mod portfolio;
16pub mod schedule;
17pub mod stop;
18
19use std::time::Instant;
20
21use brokapi_bcs_protocol::models::instrument::InstrumentKey as BcsInstrumentKey;
22use brokapi_core::{InstrumentId, InstrumentKey, NativeId, Timestamps, UnixNanos};
23use chrono::{DateTime, Utc};
24
25/// The order quantity as the whole number of pieces the BCS order API takes.
26///
27/// BCS counts an order in pieces (`orderQuantity`, "Количество в заявке (шт.)", an integer of at
28/// least 1), and so does [`Qty`](brokapi_core::Qty) everywhere in this library: the number is passed
29/// on as it is. A quantity that is not a whole number of pieces is refused, never rounded: a rounded
30/// order would buy something the caller did not ask for.
31///
32/// # Errors
33///
34/// [`brokapi_core::Error::Rejected`] with code `invalid_qty` for a fraction, zero, a negative number or
35/// a number that does not fit in an `i64`.
36///
37/// # Examples
38///
39/// ```
40/// use brokapi_bcs::map::whole_pieces;
41/// use brokapi_core::Qty;
42///
43/// assert_eq!(whole_pieces(Qty::from(10))?, 10);
44/// assert_eq!(whole_pieces("10.0".parse()?)?, 10);
45/// // 2.5 is not rounded to 3: it is refused.
46/// assert!(whole_pieces("2.5".parse()?).is_err());
47/// assert!(whole_pieces("0.4".parse()?).is_err());
48/// assert!(whole_pieces(Qty::ZERO).is_err());
49/// # Ok::<(), Box<dyn std::error::Error>>(())
50/// ```
51pub fn whole_pieces(qty: brokapi_core::Qty) -> Result<i64, brokapi_core::Error> {
52    match qty.to_whole() {
53        Some(n) if n >= 1 => Ok(n),
54        _ => Err(brokapi_core::Error::Rejected {
55            code: "invalid_qty".into(),
56            message: format!("quantity {qty} is not a positive whole number of pieces"),
57        }),
58    }
59}
60
61/// Builds an [`InstrumentId`] from a BCS instrument key. The native id is
62/// [`NativeId::TickerClass`], the form every BCS call wants.
63///
64/// # Examples
65///
66/// ```
67/// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
68/// use brokapi_bcs::map::instrument_id;
69/// use brokapi_core::NativeId;
70///
71/// let id = instrument_id(&InstrumentKey::new("SBER", "TQBR"));
72/// assert!(matches!(id.native, NativeId::TickerClass { .. }));
73/// ```
74pub fn instrument_id(key: &BcsInstrumentKey) -> InstrumentId {
75    InstrumentId::new(
76        InstrumentKey::new(&key.ticker, &key.class_code),
77        NativeId::TickerClass {
78            ticker: key.ticker.clone(),
79            class_code: key.class_code.clone(),
80        },
81    )
82}
83
84/// Builds an [`InstrumentId`] from a ticker and a class code (board). Same as [`instrument_id`].
85pub fn instrument_id_ticker(ticker: &str, class_code: &str) -> InstrumentId {
86    instrument_id(&BcsInstrumentKey::new(ticker, class_code))
87}
88
89/// Builds the [`Timestamps`] of an event: the local receive time now (wall clock), the monotonic
90/// `now` of the receive, and the broker's own time when it is known.
91///
92/// The broker time is optional because not every BCS message carries one.
93pub fn timestamps(now: Instant, broker_ts: Option<UnixNanos>) -> Timestamps {
94    let rx_ts = UnixNanos::from_nanos(
95        std::time::SystemTime::now()
96            .duration_since(std::time::UNIX_EPOCH)
97            .map(|d| d.as_nanos() as i64)
98            .unwrap_or(0),
99    );
100    let mut ts = Timestamps::new(rx_ts, now);
101    if let Some(b) = broker_ts {
102        ts = ts.with_broker_ts(b);
103    }
104    ts
105}
106
107/// Parses a time string of the BCS API into nanoseconds since the Unix epoch.
108///
109/// Reads RFC 3339 (`2026-10-19T10:00:00.123+03:00`) and the form `2026-10-19T10:00:00.123Z`. `None`
110/// when the input is `None` or in neither form. A time with no zone is not accepted, because its zone
111/// would be a guess.
112///
113/// # Examples
114///
115/// ```
116/// use brokapi_bcs::map::parse_broker_ts;
117///
118/// let t = parse_broker_ts(Some("2026-10-19T10:00:00.500Z")).expect("a valid time");
119/// assert_eq!(t.as_nanos() % 1_000_000_000, 500_000_000);
120/// assert!(parse_broker_ts(Some("yesterday")).is_none());
121/// assert!(parse_broker_ts(None).is_none());
122/// ```
123pub fn parse_broker_ts(raw: Option<&str>) -> Option<UnixNanos> {
124    let s = raw?;
125    DateTime::parse_from_rfc3339(s)
126        .ok()
127        .map(|dt| unix_nanos(dt.with_timezone(&Utc)))
128        .or_else(|| {
129            chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3fZ")
130                .ok()
131                .map(|ndt| unix_nanos(DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc)))
132        })
133}
134
135/// Converts a UTC date and time to nanoseconds since the Unix epoch. A date outside the range that
136/// fits in 64 bits (years 1677 to 2262) gives `0`.
137pub fn unix_nanos(dt: DateTime<Utc>) -> UnixNanos {
138    UnixNanos::from_nanos(dt.timestamp_nanos_opt().unwrap_or(0))
139}
140
141/// Returns the BCS instrument key of an [`InstrumentId`], or `None` when its native id is not
142/// [`NativeId::TickerClass`] (the id came from another broker).
143///
144/// # Examples
145///
146/// ```
147/// use brokapi_bcs::map::{bcs_instrument_key, instrument_id_ticker};
148///
149/// let id = instrument_id_ticker("SBER", "TQBR");
150/// assert_eq!(bcs_instrument_key(&id).unwrap().ticker, "SBER");
151/// ```
152pub fn bcs_instrument_key(id: &InstrumentId) -> Option<BcsInstrumentKey> {
153    match &id.native {
154        NativeId::TickerClass { ticker, class_code } => {
155            Some(BcsInstrumentKey::new(ticker.clone(), class_code.clone()))
156        }
157        _ => None,
158    }
159}