Skip to main content

brokapi_bcs/
util.rs

1//! Small helpers shared by the two connectors: instrument ids, timeframes and timestamps.
2
3use brokapi_bcs_protocol::models::instrument::InstrumentKey;
4use brokapi_core::{InstrumentId, NativeId, Timeframe, UnixNanos};
5use chrono::{DateTime, TimeZone, Utc};
6
7use crate::map::bcs_instrument_key;
8
9/// Returns the BCS instrument key (ticker and class code) of an instrument id.
10///
11/// BCS names an instrument by ticker plus trading board (class code), for example `SBER` on `TQBR`.
12///
13/// # Errors
14///
15/// [`brokapi_core::Error::Config`] when the native id of `id` is not [`NativeId::TickerClass`]. An id
16/// that came from another broker cannot be used here.
17///
18/// # Examples
19///
20/// ```
21/// use brokapi_bcs::util::require_ticker_class;
22/// use brokapi_core::{InstrumentId, InstrumentKey, NativeId};
23///
24/// let sber = InstrumentId::new(
25///     InstrumentKey::new("SBER", "TQBR"),
26///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
27/// );
28/// assert_eq!(require_ticker_class(&sber).unwrap().class_code, "TQBR");
29///
30/// let foreign = InstrumentId::new(InstrumentKey::new("SBER", "MISX"), NativeId::Uid("uid".into()));
31/// assert!(require_ticker_class(&foreign).is_err());
32/// ```
33pub fn require_ticker_class(id: &InstrumentId) -> Result<InstrumentKey, brokapi_core::Error> {
34    bcs_instrument_key(id).ok_or_else(|| {
35        brokapi_core::Error::Config(brokapi_core::ConfigError::Message(
36            "instrument native id must be TickerClass for BCS".into(),
37        ))
38    })
39}
40
41/// The label of a timeframe in BCS requests: `M1 M5 M15 M30 H1 H4 D W MN` (docs, checked 2026-10-19).
42/// `M10` is not offered by BCS: `None`, the caller answers `Unsupported`.
43pub fn timeframe_label(tf: Timeframe) -> Option<&'static str> {
44    Some(match tf {
45        Timeframe::M1 => "M1",
46        Timeframe::M5 => "M5",
47        Timeframe::M10 => return None,
48        Timeframe::M15 => "M15",
49        Timeframe::M30 => "M30",
50        Timeframe::H1 => "H1",
51        Timeframe::H4 => "H4",
52        Timeframe::D1 => "D",
53        Timeframe::W1 => "W",
54        Timeframe::MN => "MN",
55    })
56}
57
58/// Converts a time in nanoseconds since the Unix epoch to a UTC date and time.
59///
60/// Sub-second precision is kept. A value that is out of the range of dates falls back to the current
61/// time, so this function never fails (and never panics).
62///
63/// # Examples
64///
65/// ```
66/// use brokapi_bcs::util::nanos_to_datetime;
67/// use brokapi_core::UnixNanos;
68///
69/// let dt = nanos_to_datetime(UnixNanos::from_nanos(1_000_000_000));
70/// assert_eq!(dt.timestamp(), 1);
71/// ```
72pub fn nanos_to_datetime(nanos: UnixNanos) -> DateTime<Utc> {
73    let secs = nanos.as_nanos() / 1_000_000_000;
74    let nsec = (nanos.as_nanos() % 1_000_000_000) as u32;
75    match Utc.timestamp_opt(secs, nsec) {
76        chrono::LocalResult::Single(dt) => dt,
77        chrono::LocalResult::Ambiguous(dt, _) => dt,
78        chrono::LocalResult::None => Utc::now(),
79    }
80}
81
82/// Returns the class code (trading board, for example `TQBR`) of an instrument id.
83///
84/// # Errors
85///
86/// [`brokapi_core::Error::Config`] when the native id is not [`NativeId::TickerClass`], as in
87/// [`require_ticker_class`].
88///
89/// # Examples
90///
91/// ```
92/// use brokapi_bcs::util::class_code;
93/// use brokapi_core::{InstrumentId, InstrumentKey, NativeId};
94///
95/// let sber = InstrumentId::new(
96///     InstrumentKey::new("SBER", "TQBR"),
97///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
98/// );
99/// assert_eq!(class_code(&sber).unwrap(), "TQBR");
100/// ```
101pub fn class_code(id: &InstrumentId) -> Result<String, brokapi_core::Error> {
102    match &id.native {
103        NativeId::TickerClass { class_code, .. } => Ok(class_code.clone()),
104        _ => require_ticker_class(id).map(|k| k.class_code),
105    }
106}
107
108/// Returns the ticker (for example `SBER`) of an instrument id.
109///
110/// # Errors
111///
112/// [`brokapi_core::Error::Config`] when the native id is not [`NativeId::TickerClass`], as in
113/// [`require_ticker_class`].
114///
115/// # Examples
116///
117/// ```
118/// use brokapi_bcs::util::ticker;
119/// use brokapi_core::{InstrumentId, InstrumentKey, NativeId};
120///
121/// let sber = InstrumentId::new(
122///     InstrumentKey::new("SBER", "TQBR"),
123///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
124/// );
125/// assert_eq!(ticker(&sber).unwrap(), "SBER");
126/// ```
127pub fn ticker(id: &InstrumentId) -> Result<String, brokapi_core::Error> {
128    match &id.native {
129        NativeId::TickerClass { ticker, .. } => Ok(ticker.clone()),
130        _ => require_ticker_class(id).map(|k| k.ticker),
131    }
132}