nautilus_binance/common/
instruments.rs1use std::str::FromStr;
19
20use ahash::AHashSet;
21use nautilus_model::identifiers::InstrumentId;
22
23use crate::config::BinanceInstrumentProviderConfig;
24
25#[derive(Debug, Clone)]
27pub struct BinanceInstrumentSelector {
28 load_all: bool,
29 load_ids: AHashSet<InstrumentId>,
30 symbols: Option<AHashSet<String>>,
31 bases: Option<AHashSet<String>>,
32 quotes: Option<AHashSet<String>>,
33 contract_types: Option<AHashSet<String>>,
34}
35
36impl BinanceInstrumentSelector {
37 pub fn new(config: &BinanceInstrumentProviderConfig) -> anyhow::Result<Self> {
43 let load_ids = config
44 .load_ids
45 .as_deref()
46 .unwrap_or_default()
47 .iter()
48 .map(|value| InstrumentId::from_str(value))
49 .collect::<Result<AHashSet<_>, _>>()?;
50
51 Ok(Self {
52 load_all: config.load_all,
53 load_ids,
54 symbols: filter_values(config, "symbols")?,
55 bases: filter_values(config, "bases")?,
56 quotes: filter_values(config, "quotes")?,
57 contract_types: filter_values(config, "contract_types")?,
58 })
59 }
60
61 #[must_use]
63 pub fn includes(
64 &self,
65 instrument_id: InstrumentId,
66 symbol: &str,
67 base: &str,
68 quote: &str,
69 contract_type: Option<&str>,
70 ) -> bool {
71 if !self.load_all && !self.load_ids.contains(&instrument_id) {
72 return false;
73 }
74
75 matches_filter(&self.symbols, symbol)
76 && matches_filter(&self.bases, base)
77 && matches_filter(&self.quotes, quote)
78 && self
79 .contract_types
80 .as_ref()
81 .is_none_or(|values| contract_type.is_some_and(|value| contains(values, value)))
82 }
83}
84
85fn filter_values(
86 config: &BinanceInstrumentProviderConfig,
87 key: &str,
88) -> anyhow::Result<Option<AHashSet<String>>> {
89 let Some(value) = config.filters.get(key) else {
90 return Ok(None);
91 };
92
93 let values = match value {
94 serde_json::Value::String(value) => vec![value.as_str()],
95 serde_json::Value::Array(values) => values
96 .iter()
97 .map(|value| {
98 value
99 .as_str()
100 .ok_or_else(|| anyhow::anyhow!("filter {key:?} contains a non-string value"))
101 })
102 .collect::<anyhow::Result<Vec<_>>>()?,
103 _ => anyhow::bail!("filter {key:?} is not a string or array"),
104 };
105
106 Ok(Some(
107 values
108 .into_iter()
109 .map(|value| value.trim().to_ascii_uppercase())
110 .collect(),
111 ))
112}
113
114fn matches_filter(values: &Option<AHashSet<String>>, value: &str) -> bool {
115 values.as_ref().is_none_or(|values| contains(values, value))
116}
117
118fn contains(values: &AHashSet<String>, value: &str) -> bool {
119 values.contains(&value.to_ascii_uppercase())
120}
121
122#[cfg(test)]
123mod tests {
124 use std::collections::HashMap;
125
126 use nautilus_model::identifiers::InstrumentId;
127 use rstest::rstest;
128
129 use super::*;
130
131 #[rstest]
132 fn test_selector_load_ids_and_filters_are_both_discriminating() {
133 let config = BinanceInstrumentProviderConfig {
134 load_all: false,
135 load_ids: Some(vec!["ETHUSDT-PERP.BINANCE".to_string()]),
136 filters: HashMap::from([
137 ("bases".to_string(), serde_json::json!(["eth"])),
138 ("quotes".to_string(), serde_json::json!("usdt")),
139 (
140 "contract_types".to_string(),
141 serde_json::json!(["PERPETUAL"]),
142 ),
143 ]),
144 ..Default::default()
145 };
146 let selector = BinanceInstrumentSelector::new(&config).unwrap();
147
148 assert!(selector.includes(
149 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
150 "ETHUSDT",
151 "ETH",
152 "USDT",
153 Some("PERPETUAL"),
154 ));
155 assert!(!selector.includes(
156 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
157 "BTCUSDT",
158 "BTC",
159 "USDT",
160 Some("PERPETUAL"),
161 ));
162 assert!(!selector.includes(
163 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
164 "ETHUSDT",
165 "ETH",
166 "USDT",
167 Some("CURRENT_QUARTER"),
168 ));
169 }
170
171 #[rstest]
172 fn test_selector_load_all_still_applies_venue_filters() {
173 let config = BinanceInstrumentProviderConfig {
174 filters: HashMap::from([("symbols".to_string(), serde_json::json!(["btcusdt"]))]),
175 ..Default::default()
176 };
177 let selector = BinanceInstrumentSelector::new(&config).unwrap();
178
179 assert!(selector.includes(
180 InstrumentId::from("BTCUSDT.BINANCE"),
181 "BTCUSDT",
182 "BTC",
183 "USDT",
184 None,
185 ));
186 assert!(!selector.includes(
187 InstrumentId::from("ETHUSDT.BINANCE"),
188 "ETHUSDT",
189 "ETH",
190 "USDT",
191 None,
192 ));
193 }
194}