1use std::collections::HashMap;
24
25use bullet_exchange_interface::types::MarketId;
26
27use crate::generated::types::Symbol;
28
29#[derive(Debug, Clone)]
31pub(crate) struct ExchangeMetadata {
32 symbols: Vec<SymbolInfo>,
33 by_name: HashMap<String, usize>,
35 by_id: HashMap<u16, usize>,
37}
38
39#[derive(Debug, Clone)]
41pub struct SymbolInfo {
42 pub symbol: String,
44 pub market_id: MarketId,
46 pub status: String,
48 pub base_asset: String,
50 pub quote_asset: String,
52 pub price_precision: u8,
54 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}