1use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, RwLock};
7
8use rust_decimal::Decimal;
9use serde::Deserialize;
10use tesser_broker::{BrokerError, ExecutionClient};
11use tesser_core::{AssetId, ExchangeId, Instrument, InstrumentKind, Price, Quantity, Symbol};
12use thiserror::Error;
13
14#[derive(Clone, Default)]
16pub struct MarketRegistry {
17 inner: Arc<RwLock<HashMap<Symbol, Instrument>>>,
18}
19
20impl MarketRegistry {
21 pub fn from_instruments(instruments: Vec<Instrument>) -> Result<Self, MarketRegistryError> {
23 if instruments.is_empty() {
24 return Err(MarketRegistryError::Empty);
25 }
26 let mut map = HashMap::new();
27 for instrument in instruments {
28 map.insert(instrument.symbol, instrument);
29 }
30 Ok(Self {
31 inner: Arc::new(RwLock::new(map)),
32 })
33 }
34
35 pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, MarketRegistryError> {
37 let mut catalog = InstrumentCatalog::new();
38 catalog.add_file(path)?;
39 catalog.build()
40 }
41
42 pub async fn load_from_exchange(
44 client: &(dyn ExecutionClient + Send + Sync),
45 category: &str,
46 ) -> Result<Self, MarketRegistryError> {
47 let instruments = client
48 .list_instruments(category)
49 .await
50 .map_err(MarketRegistryError::Broker)?;
51 Self::from_instruments(instruments)
52 }
53
54 pub fn get(&self, symbol: impl Into<Symbol>) -> Option<Instrument> {
56 let symbol = symbol.into();
57 self.inner
58 .read()
59 .ok()
60 .and_then(|map| map.get(&symbol).cloned())
61 }
62
63 pub fn validate_symbols<I, S>(&self, symbols: I) -> Result<(), MarketRegistryError>
65 where
66 I: IntoIterator<Item = S>,
67 S: Into<Symbol>,
68 {
69 let map = self
70 .inner
71 .read()
72 .map_err(|_| MarketRegistryError::Poisoned)?;
73 for symbol in symbols {
74 let symbol = symbol.into();
75 if !map.contains_key(&symbol) {
76 return Err(MarketRegistryError::UnknownSymbol(symbol));
77 }
78 }
79 Ok(())
80 }
81
82 pub fn instruments(&self) -> Vec<Instrument> {
84 self.inner
85 .read()
86 .map(|map| map.values().cloned().collect())
87 .unwrap_or_default()
88 }
89
90 pub fn normalize_pair_quantity(
92 &self,
93 first: Symbol,
94 second: Symbol,
95 quantity: Quantity,
96 ) -> Option<Quantity> {
97 let first_instr = self.get(first)?;
98 let second_instr = self.get(second)?;
99 let step = first_instr.lot_size.max(second_instr.lot_size);
100 if step <= Decimal::ZERO {
101 return Some(quantity);
102 }
103 let normalized = (quantity / step).floor() * step;
104 Some(normalized)
105 }
106}
107
108#[derive(Debug, Error)]
109pub enum MarketRegistryError {
110 #[error("no instruments were loaded into the registry")]
111 Empty,
112 #[error("unknown symbol '{0}'")]
113 UnknownSymbol(Symbol),
114 #[error("instrument '{symbol}' missing required field '{field}'")]
115 MissingField { symbol: Symbol, field: &'static str },
116 #[error("instrument '{symbol}' has conflicting value for '{field}'")]
117 ConflictingField { symbol: Symbol, field: &'static str },
118 #[error("markets file is invalid: {0}")]
119 InvalidFormat(toml::de::Error),
120 #[error("failed to read markets file at {path}: {source}")]
121 Io {
122 path: PathBuf,
123 #[source]
124 source: std::io::Error,
125 },
126 #[error("market registry poisoned by concurrent access")]
127 Poisoned,
128 #[error("broker error: {0}")]
129 Broker(BrokerError),
130}
131
132#[derive(Default)]
134pub struct InstrumentCatalog {
135 entries: HashMap<Symbol, InstrumentInfo>,
136}
137
138impl InstrumentCatalog {
139 #[must_use]
140 pub fn new() -> Self {
141 Self {
142 entries: HashMap::new(),
143 }
144 }
145
146 pub fn add_file(&mut self, path: impl AsRef<Path>) -> Result<(), MarketRegistryError> {
147 let path = path.as_ref();
148 let contents = fs::read_to_string(path).map_err(|err| MarketRegistryError::Io {
149 path: path.to_path_buf(),
150 source: err,
151 })?;
152 let file: MarketFile =
153 toml::from_str(&contents).map_err(MarketRegistryError::InvalidFormat)?;
154 for raw in file.markets {
155 self.insert(raw.into_typed())?;
156 }
157 Ok(())
158 }
159
160 pub fn add_instruments(
161 &mut self,
162 instruments: Vec<Instrument>,
163 ) -> Result<(), MarketRegistryError> {
164 for instrument in instruments {
165 self.insert(InstrumentInfo::from(instrument))?;
166 }
167 Ok(())
168 }
169
170 #[must_use]
171 pub fn is_empty(&self) -> bool {
172 self.entries.is_empty()
173 }
174
175 pub fn build(self) -> Result<MarketRegistry, MarketRegistryError> {
176 let instruments = self
177 .entries
178 .into_values()
179 .map(|info| info.into_instrument())
180 .collect::<Result<Vec<_>, _>>()?;
181 MarketRegistry::from_instruments(instruments)
182 }
183
184 fn insert(&mut self, info: InstrumentInfo) -> Result<(), MarketRegistryError> {
185 let key = info.key();
186 if let Some(existing) = self.entries.remove(&key) {
187 let merged = existing.merge(info)?;
188 self.entries.insert(key, merged);
189 } else {
190 self.entries.insert(key, info);
191 }
192 Ok(())
193 }
194}
195
196#[derive(Deserialize)]
197struct MarketFile {
198 #[serde(default)]
199 markets: Vec<RawInstrumentInfo>,
200}
201
202#[derive(Clone, Debug, Deserialize)]
203struct RawInstrumentInfo {
204 exchange: ExchangeId,
205 symbol: String,
206 #[serde(default)]
207 base: Option<String>,
208 #[serde(default)]
209 quote: Option<String>,
210 #[serde(default)]
211 settlement_currency: Option<String>,
212 #[serde(default)]
213 kind: Option<InstrumentKind>,
214 #[serde(default)]
215 tick_size: Option<Price>,
216 #[serde(default)]
217 lot_size: Option<Quantity>,
218}
219
220impl RawInstrumentInfo {
221 fn into_typed(self) -> InstrumentInfo {
222 let exchange = self.exchange;
223 let symbol = Symbol::from_code(exchange, self.symbol);
224 InstrumentInfo {
225 symbol,
226 base: self.base.map(|code| AssetId::from_code(exchange, code)),
227 quote: self.quote.map(|code| AssetId::from_code(exchange, code)),
228 settlement_currency: self
229 .settlement_currency
230 .map(|code| AssetId::from_code(exchange, code)),
231 kind: self.kind,
232 tick_size: self.tick_size,
233 lot_size: self.lot_size,
234 }
235 }
236}
237
238#[derive(Clone, Debug)]
239struct InstrumentInfo {
240 symbol: Symbol,
241 base: Option<AssetId>,
242 quote: Option<AssetId>,
243 settlement_currency: Option<AssetId>,
244 kind: Option<InstrumentKind>,
245 tick_size: Option<Price>,
246 lot_size: Option<Quantity>,
247}
248
249impl InstrumentInfo {
250 fn key(&self) -> Symbol {
251 self.symbol
252 }
253
254 fn merge(self, other: InstrumentInfo) -> Result<InstrumentInfo, MarketRegistryError> {
255 let symbol = self.key();
256 if symbol != other.key() {
257 return Err(MarketRegistryError::ConflictingField {
258 symbol,
259 field: "symbol",
260 });
261 }
262 Ok(InstrumentInfo {
263 symbol,
264 base: merge_field(symbol, "base", self.base, other.base)?,
265 quote: merge_field(symbol, "quote", self.quote, other.quote)?,
266 settlement_currency: merge_field(
267 symbol,
268 "settlement_currency",
269 self.settlement_currency,
270 other.settlement_currency,
271 )?,
272 kind: merge_field(symbol, "kind", self.kind, other.kind)?,
273 tick_size: merge_field(symbol, "tick_size", self.tick_size, other.tick_size)?,
274 lot_size: merge_field(symbol, "lot_size", self.lot_size, other.lot_size)?,
275 })
276 }
277
278 fn into_instrument(self) -> Result<Instrument, MarketRegistryError> {
279 let symbol = self.symbol;
280 let base = require_field(symbol, "base", self.base)?;
281 let quote = require_field(symbol, "quote", self.quote)?;
282 let settlement = require_field(symbol, "settlement_currency", self.settlement_currency)?;
283 let kind = require_field(symbol, "kind", self.kind)?;
284 let tick_size = require_field(symbol, "tick_size", self.tick_size)?;
285 let lot_size = require_field(symbol, "lot_size", self.lot_size)?;
286
287 Ok(Instrument {
288 symbol,
289 base,
290 quote,
291 kind,
292 settlement_currency: settlement,
293 tick_size,
294 lot_size,
295 })
296 }
297}
298
299fn merge_field<T: Clone + PartialEq>(
300 symbol: Symbol,
301 field: &'static str,
302 primary: Option<T>,
303 secondary: Option<T>,
304) -> Result<Option<T>, MarketRegistryError> {
305 match (primary, secondary) {
306 (Some(existing), Some(replacement)) => {
307 if existing != replacement {
308 Err(MarketRegistryError::ConflictingField { symbol, field })
309 } else {
310 Ok(Some(existing))
311 }
312 }
313 (Some(existing), None) => Ok(Some(existing)),
314 (None, Some(value)) => Ok(Some(value)),
315 (None, None) => Ok(None),
316 }
317}
318
319fn require_field<T>(
320 symbol: Symbol,
321 field: &'static str,
322 value: Option<T>,
323) -> Result<T, MarketRegistryError> {
324 value.ok_or(MarketRegistryError::MissingField { symbol, field })
325}
326
327impl From<Instrument> for InstrumentInfo {
328 fn from(value: Instrument) -> Self {
329 Self {
330 symbol: value.symbol,
331 base: Some(value.base),
332 quote: Some(value.quote),
333 settlement_currency: Some(value.settlement_currency),
334 kind: Some(value.kind),
335 tick_size: Some(value.tick_size),
336 lot_size: Some(value.lot_size),
337 }
338 }
339}