Skip to main content

bullet_rust_sdk/
metadata.rs

1//! Exchange metadata: symbol lookups and market info.
2//!
3//! Metadata is fetched from the exchange during [`Client`] construction and
4//! cached for the lifetime of the client. Use [`Client::refresh_metadata`] to
5//! update it for long-running processes.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use bullet_rust_sdk::{Client, MarketId};
11//!
12//! let client = Client::mainnet().await?;
13//!
14//! // Resolve symbol string to numeric MarketId
15//! let market_id = client.market_id("BTC-USD").expect("unknown symbol");
16//!
17//! // Get all symbols
18//! for sym in client.symbols() {
19//!     println!("{}: MarketId({})", sym.symbol, sym.market_id.0);
20//! }
21//! ```
22
23use std::collections::HashMap;
24
25use bullet_exchange_interface::types::MarketId;
26
27use crate::generated::types::Symbol;
28
29/// Cached exchange metadata for fast symbol lookups.
30#[derive(Debug, Clone)]
31pub(crate) struct ExchangeMetadata {
32    symbols: Vec<SymbolInfo>,
33    /// symbol string -> index into `symbols`
34    by_name: HashMap<String, usize>,
35    /// market_id.0 -> index into `symbols`
36    by_id: HashMap<u16, usize>,
37}
38
39/// Symbol information cached from the exchange.
40#[derive(Debug, Clone)]
41pub struct SymbolInfo {
42    /// Trading pair symbol (e.g. `"BTC-USD"`).
43    pub symbol: String,
44    /// Numeric market identifier.
45    pub market_id: MarketId,
46    /// Trading status (e.g. `"TRADING"`, `"HALT"`).
47    pub status: String,
48    /// Base asset (e.g. `"BTC"`).
49    pub base_asset: String,
50    /// Quote asset (e.g. `"USD"`).
51    pub quote_asset: String,
52    /// Price decimal precision.
53    pub price_precision: u8,
54    /// Quantity decimal precision.
55    pub quantity_precision: u8,
56}
57
58impl ExchangeMetadata {
59    pub(crate) fn from_symbols(raw: &[Symbol]) -> Self {
60        let symbols: Vec<SymbolInfo> = raw
61            .iter()
62            .map(|s| SymbolInfo {
63                symbol: s.symbol.clone(),
64                market_id: MarketId(s.market_id),
65                status: s.status.clone(),
66                base_asset: s.base_asset.clone(),
67                quote_asset: s.quote_asset.clone(),
68                price_precision: s.price_precision,
69                quantity_precision: s.quantity_precision,
70            })
71            .collect();
72
73        let by_name = symbols.iter().enumerate().map(|(i, s)| (s.symbol.clone(), i)).collect();
74
75        let by_id = symbols.iter().enumerate().map(|(i, s)| (s.market_id.0, i)).collect();
76
77        Self { symbols, by_name, by_id }
78    }
79
80    pub(crate) fn market_id(&self, symbol: &str) -> Option<MarketId> {
81        self.by_name.get(symbol).map(|&i| self.symbols[i].market_id)
82    }
83
84    pub(crate) fn symbol_info_by_name(&self, symbol: &str) -> Option<&SymbolInfo> {
85        self.by_name.get(symbol).map(|&i| &self.symbols[i])
86    }
87
88    pub(crate) fn symbol_info_by_id(&self, market_id: MarketId) -> Option<&SymbolInfo> {
89        self.by_id.get(&market_id.0).map(|&i| &self.symbols[i])
90    }
91
92    pub(crate) fn symbols(&self) -> &[SymbolInfo] {
93        &self.symbols
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::generated::types::Symbol;
101
102    fn mock_symbols() -> Vec<Symbol> {
103        vec![
104            Symbol {
105                symbol: "BTC-USD".into(),
106                market_id: 0,
107                status: "TRADING".into(),
108                base_asset: "BTC".into(),
109                quote_asset: "USD".into(),
110                price_precision: 2,
111                quantity_precision: 3,
112                pair: "BTCUSD".into(),
113                contract_type: "PERPETUAL".into(),
114                delivery_date: 0,
115                onboard_date: 0,
116                margin_asset: "USD".into(),
117                base_asset_precision: 8,
118                quote_precision: 8,
119                underlying_type: "COIN".into(),
120                underlying_sub_type: vec![],
121                settle_plan: 0,
122                trigger_protect: Default::default(),
123                filters: vec![],
124                order_types: vec![],
125                time_in_force: vec![],
126                liquidation_fee: Default::default(),
127                market_take_bound: Default::default(),
128                maker_fee_bps: vec![],
129                taker_fee_bps: vec![],
130            },
131            Symbol {
132                symbol: "ETH-USD".into(),
133                market_id: 1,
134                status: "TRADING".into(),
135                base_asset: "ETH".into(),
136                quote_asset: "USD".into(),
137                price_precision: 2,
138                quantity_precision: 4,
139                pair: "ETHUSD".into(),
140                contract_type: "PERPETUAL".into(),
141                delivery_date: 0,
142                onboard_date: 0,
143                margin_asset: "USD".into(),
144                base_asset_precision: 8,
145                quote_precision: 8,
146                underlying_type: "COIN".into(),
147                underlying_sub_type: vec![],
148                settle_plan: 0,
149                trigger_protect: Default::default(),
150                filters: vec![],
151                order_types: vec![],
152                time_in_force: vec![],
153                liquidation_fee: Default::default(),
154                market_take_bound: Default::default(),
155                maker_fee_bps: vec![],
156                taker_fee_bps: vec![],
157            },
158        ]
159    }
160
161    #[test]
162    fn market_id_lookup() {
163        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
164        assert_eq!(meta.market_id("BTC-USD"), Some(MarketId(0)));
165        assert_eq!(meta.market_id("ETH-USD"), Some(MarketId(1)));
166        assert_eq!(meta.market_id("SOL-USD"), None);
167    }
168
169    #[test]
170    fn symbol_info_by_name_lookup() {
171        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
172        let info = meta.symbol_info_by_name("ETH-USD").unwrap();
173        assert_eq!(info.base_asset, "ETH");
174        assert_eq!(info.quantity_precision, 4);
175    }
176
177    #[test]
178    fn symbol_info_by_id_lookup() {
179        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
180        let info = meta.symbol_info_by_id(MarketId(0)).unwrap();
181        assert_eq!(info.symbol, "BTC-USD");
182        assert!(meta.symbol_info_by_id(MarketId(99)).is_none());
183    }
184
185    #[test]
186    fn symbols_returns_all() {
187        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
188        assert_eq!(meta.symbols().len(), 2);
189    }
190
191    #[test]
192    fn empty_symbols() {
193        let meta = ExchangeMetadata::from_symbols(&[]);
194        assert!(meta.symbols().is_empty());
195        assert_eq!(meta.market_id("BTC-USD"), None);
196    }
197}