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                base_asset_id: 0,
110                quote_asset: "USD".into(),
111                price_precision: 2,
112                quantity_precision: 3,
113                pair: "BTCUSD".into(),
114                contract_type: "PERPETUAL".into(),
115                delivery_date: 0,
116                onboard_date: 0,
117                margin_asset: "USD".into(),
118                base_asset_precision: 8,
119                quote_precision: 8,
120                underlying_type: "COIN".into(),
121                underlying_sub_type: vec![],
122                settle_plan: 0,
123                trigger_protect: Default::default(),
124                filters: vec![],
125                order_types: vec![],
126                time_in_force: vec![],
127                liquidation_fee: Default::default(),
128                market_take_bound: Default::default(),
129                maker_fee_bps: vec![],
130                taker_fee_bps: vec![],
131            },
132            Symbol {
133                symbol: "ETH-USD".into(),
134                market_id: 1,
135                status: "TRADING".into(),
136                base_asset: "ETH".into(),
137                base_asset_id: 1,
138                quote_asset: "USD".into(),
139                price_precision: 2,
140                quantity_precision: 4,
141                pair: "ETHUSD".into(),
142                contract_type: "PERPETUAL".into(),
143                delivery_date: 0,
144                onboard_date: 0,
145                margin_asset: "USD".into(),
146                base_asset_precision: 8,
147                quote_precision: 8,
148                underlying_type: "COIN".into(),
149                underlying_sub_type: vec![],
150                settle_plan: 0,
151                trigger_protect: Default::default(),
152                filters: vec![],
153                order_types: vec![],
154                time_in_force: vec![],
155                liquidation_fee: Default::default(),
156                market_take_bound: Default::default(),
157                maker_fee_bps: vec![],
158                taker_fee_bps: vec![],
159            },
160        ]
161    }
162
163    #[test]
164    fn market_id_lookup() {
165        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
166        assert_eq!(meta.market_id("BTC-USD"), Some(MarketId(0)));
167        assert_eq!(meta.market_id("ETH-USD"), Some(MarketId(1)));
168        assert_eq!(meta.market_id("SOL-USD"), None);
169    }
170
171    #[test]
172    fn symbol_info_by_name_lookup() {
173        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
174        let info = meta.symbol_info_by_name("ETH-USD").unwrap();
175        assert_eq!(info.base_asset, "ETH");
176        assert_eq!(info.quantity_precision, 4);
177    }
178
179    #[test]
180    fn symbol_info_by_id_lookup() {
181        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
182        let info = meta.symbol_info_by_id(MarketId(0)).unwrap();
183        assert_eq!(info.symbol, "BTC-USD");
184        assert!(meta.symbol_info_by_id(MarketId(99)).is_none());
185    }
186
187    #[test]
188    fn symbols_returns_all() {
189        let meta = ExchangeMetadata::from_symbols(&mock_symbols());
190        assert_eq!(meta.symbols().len(), 2);
191    }
192
193    #[test]
194    fn empty_symbols() {
195        let meta = ExchangeMetadata::from_symbols(&[]);
196        assert!(meta.symbols().is_empty());
197        assert_eq!(meta.market_id("BTC-USD"), None);
198    }
199}