Skip to main content

brokapi_bybit/
util.rs

1//! Small helpers: instrument ids, intervals, times.
2
3use brokapi_core::{
4    ConfigError, Error, InstrumentId, InstrumentKey, NativeId, Timeframe, UnixNanos,
5};
6
7/// The id of an instrument: the symbol and the category.
8///
9/// The key is `BTCUSDT@linear`: the second part names the product category of the exchange, because the same symbol
10/// can trade as spot and as a perpetual. The native id is the pair as the exchange spells it.
11///
12/// # Examples
13///
14/// ```
15/// use brokapi_bybit::util::instrument_id;
16///
17/// let btc = instrument_id("BTCUSDT", "linear");
18/// assert_eq!(btc.key.display(), "BTCUSDT@linear");
19/// ```
20pub fn instrument_id(symbol: &str, category: &str) -> InstrumentId {
21    InstrumentId::new(
22        InstrumentKey::new(symbol, category),
23        NativeId::SymbolMic {
24            symbol: symbol.to_string(),
25            mic: category.to_string(),
26        },
27    )
28}
29
30/// The symbol and the category of an instrument id.
31///
32/// # Errors
33///
34/// [`Error::Config`] when the native id is not of the kind this connector uses (`SymbolMic`), or the category is not
35/// the one the connector works with.
36pub fn symbol_category(id: &InstrumentId, category: &str) -> Result<String, Error> {
37    match &id.native {
38        NativeId::SymbolMic { symbol, mic } if mic == category => Ok(symbol.clone()),
39        NativeId::SymbolMic { mic, .. } => Err(Error::Config(ConfigError::Message(format!(
40            "the instrument is of the category {mic:?}, this connector works with {category:?}: start another connector for it"
41        )))),
42        _ => Err(Error::Config(ConfigError::Message(
43            "the native id of an instrument must be SymbolMic (symbol and category) for Bybit"
44                .into(),
45        ))),
46    }
47}
48
49/// The interval word of the API for a timeframe, `None` for a timeframe the exchange has no candles for (10 minutes).
50///
51/// # Examples
52///
53/// ```
54/// use brokapi_bybit::util::interval_label;
55/// use brokapi_core::Timeframe;
56///
57/// assert_eq!(interval_label(Timeframe::H4), Some("240"));
58/// assert_eq!(interval_label(Timeframe::D1), Some("D"));
59/// assert_eq!(interval_label(Timeframe::M10), None);
60/// ```
61pub fn interval_label(tf: Timeframe) -> Option<&'static str> {
62    Some(match tf {
63        Timeframe::M1 => "1",
64        Timeframe::M5 => "5",
65        Timeframe::M10 => return None,
66        Timeframe::M15 => "15",
67        Timeframe::M30 => "30",
68        Timeframe::H1 => "60",
69        Timeframe::H4 => "240",
70        Timeframe::D1 => "D",
71        Timeframe::W1 => "W",
72        Timeframe::MN => "M",
73    })
74}
75
76/// Milliseconds since the epoch from nanoseconds.
77pub fn nanos_to_ms(nanos: UnixNanos) -> i64 {
78    nanos.as_nanos().div_euclid(1_000_000)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use brokapi_core::NativeId;
85
86    #[test]
87    fn an_id_of_another_broker_or_category_is_a_config_error() {
88        let ok = instrument_id("BTCUSDT", "linear");
89        assert_eq!(symbol_category(&ok, "linear").unwrap(), "BTCUSDT");
90        assert!(matches!(
91            symbol_category(&ok, "spot"),
92            Err(Error::Config(_))
93        ));
94        let foreign = InstrumentId::new(
95            InstrumentKey::new("SBER", "TQBR"),
96            NativeId::TickerClass {
97                ticker: "SBER".into(),
98                class_code: "TQBR".into(),
99            },
100        );
101        assert!(matches!(
102            symbol_category(&foreign, "linear"),
103            Err(Error::Config(_))
104        ));
105    }
106}