nautilus_model/identifiers/
venue.rs1use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21};
22
23use nautilus_core::correctness::{FAILED, check_valid_string};
24use ustr::Ustr;
25
26#[cfg(feature = "defi")]
27use crate::defi::{Blockchain, Chain, DexType};
28use crate::venues::VENUE_MAP;
29
30pub const SYNTHETIC_VENUE: &str = "SYNTH";
31
32#[repr(C)]
34#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
38)]
39pub struct Venue(Ustr);
40
41impl Venue {
42 pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
52 let value = value.as_ref();
53 check_valid_string(value, stringify!(value))?;
54
55 #[cfg(feature = "defi")]
56 if value.contains(':')
57 && let Err(e) = validate_blockchain_venue(value)
58 {
59 anyhow::bail!("Error creating `Venue` from '{value}': {e}");
60 }
61
62 Ok(Self(Ustr::from(value)))
63 }
64
65 pub fn new<T: AsRef<str>>(value: T) -> Self {
71 Self::new_checked(value).expect(FAILED)
72 }
73
74 #[allow(dead_code)]
76 pub(crate) fn set_inner(&mut self, value: &str) {
77 self.0 = Ustr::from(value);
78 }
79
80 #[must_use]
82 pub fn inner(&self) -> Ustr {
83 self.0
84 }
85
86 #[must_use]
88 pub fn as_str(&self) -> &str {
89 self.0.as_str()
90 }
91
92 #[must_use]
93 pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
94 Self(Ustr::from(s.as_ref()))
95 }
96
97 #[must_use]
98 pub const fn from_ustr_unchecked(s: Ustr) -> Self {
99 Self(s)
100 }
101
102 pub fn from_code(code: &str) -> anyhow::Result<Self> {
106 let map_guard = VENUE_MAP
107 .lock()
108 .map_err(|e| anyhow::anyhow!("Error acquiring lock on `VENUE_MAP`: {e}"))?;
109 map_guard
110 .get(code)
111 .copied()
112 .ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}"))
113 }
114
115 #[must_use]
116 pub fn synthetic() -> Self {
117 Self::new(SYNTHETIC_VENUE)
119 }
120
121 #[must_use]
122 pub fn is_synthetic(&self) -> bool {
123 self.0.as_str() == SYNTHETIC_VENUE
124 }
125
126 #[cfg(feature = "defi")]
128 #[must_use]
129 pub fn is_dex(&self) -> bool {
130 self.0.as_str().contains(':')
131 }
132
133 #[cfg(feature = "defi")]
134 pub fn parse_dex(&self) -> anyhow::Result<(Blockchain, DexType)> {
143 let venue_str = self.as_str();
144
145 if let Some((chain_name, dex_id)) = venue_str.split_once(':') {
146 let chain = Chain::from_chain_name(chain_name).ok_or_else(|| {
148 anyhow::anyhow!("Invalid chain '{}' in venue '{}'", chain_name, venue_str)
149 })?;
150
151 let dex_type = DexType::from_dex_name(dex_id).ok_or_else(|| {
153 anyhow::anyhow!("Invalid DEX '{}' in venue '{}'", dex_id, venue_str)
154 })?;
155
156 Ok((chain.name, dex_type))
157 } else {
158 anyhow::bail!(
159 "Venue '{}' is not a DEX venue (expected format 'Chain:DexId')",
160 venue_str
161 )
162 }
163 }
164}
165
166impl Debug for Venue {
167 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168 write!(f, "{:?}", self.0)
169 }
170}
171
172impl Display for Venue {
173 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
174 write!(f, "{}", self.0)
175 }
176}
177
178#[cfg(feature = "defi")]
186pub fn validate_blockchain_venue(venue_part: &str) -> anyhow::Result<()> {
187 if let Some((chain_name, dex_id)) = venue_part.split_once(':') {
188 if chain_name.is_empty() || dex_id.is_empty() {
189 anyhow::bail!(
190 "invalid blockchain venue '{}': expected format 'Chain:DexId'",
191 venue_part
192 );
193 }
194 if Chain::from_chain_name(chain_name).is_none() {
195 anyhow::bail!(
196 "invalid blockchain venue '{}': chain '{}' not recognized",
197 venue_part,
198 chain_name
199 );
200 }
201 if DexType::from_dex_name(dex_id).is_none() {
202 anyhow::bail!(
203 "invalid blockchain venue '{}': dex '{}' not recognized",
204 venue_part,
205 dex_id
206 );
207 }
208 Ok(())
209 } else {
210 anyhow::bail!(
211 "invalid blockchain venue '{}': expected format 'Chain:DexId'",
212 venue_part
213 );
214 }
215}
216
217#[cfg(test)]
221mod tests {
222 use rstest::rstest;
223
224 #[cfg(feature = "defi")]
225 use crate::defi::{Blockchain, DexType};
226 use crate::identifiers::{Venue, stubs::*};
227
228 #[rstest]
229 fn test_string_reprs(venue_binance: Venue) {
230 assert_eq!(venue_binance.as_str(), "BINANCE");
231 assert_eq!(format!("{venue_binance}"), "BINANCE");
232 }
233
234 #[cfg(feature = "defi")]
235 #[rstest]
236 fn test_blockchain_venue_valid_dex_names() {
237 let valid_dexes = vec![
239 "UniswapV3",
240 "UniswapV2",
241 "UniswapV4",
242 "SushiSwapV2",
243 "SushiSwapV3",
244 "PancakeSwapV3",
245 "CamelotV3",
246 "CurveFinance",
247 "FluidDEX",
248 "MaverickV1",
249 "MaverickV2",
250 "BaseX",
251 "BaseSwapV2",
252 "AerodromeV1",
253 "AerodromeSlipstream",
254 "BalancerV2",
255 "BalancerV3",
256 ];
257
258 for dex_name in valid_dexes {
259 let venue_str = format!("Arbitrum:{dex_name}");
260 let venue = Venue::new(&venue_str);
261 assert_eq!(venue.to_string(), venue_str);
262 }
263 }
264 #[cfg(feature = "defi")]
265 #[rstest]
266 #[should_panic(
267 expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
268 )]
269 fn test_blockchain_venue_invalid_chain() {
270 let _ = Venue::new("InvalidChain:UniswapV3");
271 }
272
273 #[cfg(feature = "defi")]
274 #[rstest]
275 #[should_panic(
276 expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
277 )]
278 fn test_blockchain_venue_empty_dex() {
279 let _ = Venue::new("Arbitrum:");
280 }
281
282 #[cfg(feature = "defi")]
283 #[rstest]
284 fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
285 let venue = Venue::new("Ethereum");
287 assert_eq!(venue.to_string(), "Ethereum");
288 }
289
290 #[cfg(feature = "defi")]
291 #[rstest]
292 #[should_panic(
293 expected = "Error creating `Venue` from 'Arbitrum:InvalidDex': invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
294 )]
295 fn test_blockchain_venue_invalid_dex() {
296 let _ = Venue::new("Arbitrum:InvalidDex");
297 }
298
299 #[cfg(feature = "defi")]
300 #[rstest]
301 #[should_panic(
302 expected = "Error creating `Venue` from 'Arbitrum:uniswapv3': invalid blockchain venue 'Arbitrum:uniswapv3': dex 'uniswapv3' not recognized"
303 )]
304 fn test_blockchain_venue_dex_case_sensitive() {
305 let _ = Venue::new("Arbitrum:uniswapv3");
307 }
308
309 #[cfg(feature = "defi")]
310 #[rstest]
311 fn test_blockchain_venue_various_chain_dex_combinations() {
312 let valid_combinations = vec![
314 ("Ethereum", "UniswapV2"),
315 ("Ethereum", "BalancerV2"),
316 ("Arbitrum", "CamelotV3"),
317 ("Base", "AerodromeV1"),
318 ("Polygon", "SushiSwapV3"),
319 ];
320
321 for (chain, dex) in valid_combinations {
322 let venue_str = format!("{chain}:{dex}");
323 let venue = Venue::new(&venue_str);
324 assert_eq!(venue.to_string(), venue_str);
325 }
326 }
327
328 #[cfg(feature = "defi")]
329 #[rstest]
330 #[case("Ethereum:UniswapV3", Blockchain::Ethereum, DexType::UniswapV3)]
331 #[case("Arbitrum:CamelotV3", Blockchain::Arbitrum, DexType::CamelotV3)]
332 #[case("Base:AerodromeV1", Blockchain::Base, DexType::AerodromeV1)]
333 #[case("Polygon:SushiSwapV2", Blockchain::Polygon, DexType::SushiSwapV2)]
334 fn test_parse_dex_valid(
335 #[case] venue_str: &str,
336 #[case] expected_chain: Blockchain,
337 #[case] expected_dex: DexType,
338 ) {
339 let venue = Venue::new(venue_str);
340 let (blockchain, dex_type) = venue.parse_dex().unwrap();
341
342 assert_eq!(blockchain, expected_chain);
343 assert_eq!(dex_type, expected_dex);
344 }
345
346 #[cfg(feature = "defi")]
347 #[rstest]
348 fn test_parse_dex_non_dex_venue() {
349 let venue = Venue::new("BINANCE");
350 let result = venue.parse_dex();
351 assert!(result.is_err());
352 assert!(
353 result
354 .unwrap_err()
355 .to_string()
356 .contains("is not a DEX venue")
357 );
358 }
359
360 #[cfg(feature = "defi")]
361 #[rstest]
362 fn test_parse_dex_invalid_components() {
363 let venue = Venue::from_str_unchecked("InvalidChain:UniswapV3");
365 assert!(venue.parse_dex().is_err());
366
367 let venue = Venue::from_str_unchecked("Ethereum:InvalidDex");
369 assert!(venue.parse_dex().is_err());
370 }
371}