nautilus_model/identifiers/
venue.rs1use std::{
19 fmt::{Debug, Display},
20 hash::Hash,
21};
22
23#[cfg(feature = "defi")]
24use nautilus_core::correctness::CorrectnessError;
25use nautilus_core::correctness::{
26 CorrectnessResult, CorrectnessResultExt, FAILED, check_valid_string_ascii,
27};
28use ustr::Ustr;
29
30#[cfg(feature = "defi")]
31use crate::defi::{Blockchain, Chain, DexType};
32use crate::venues::VENUE_MAP;
33
34pub const SYNTHETIC_VENUE: &str = "SYNTH";
35
36#[repr(C)]
38#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
42)]
43#[cfg_attr(
44 feature = "python",
45 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
46)]
47pub struct Venue(Ustr);
48
49impl Venue {
50 pub fn new_checked<T: AsRef<str>>(value: T) -> CorrectnessResult<Self> {
60 let value = value.as_ref();
61 check_valid_string_ascii(value, stringify!(value))?;
62
63 #[cfg(feature = "defi")]
64 if value.contains(':')
65 && let Err(e) = validate_blockchain_venue(value)
66 {
67 return Err(CorrectnessError::PredicateViolation {
68 message: format!("Error creating `Venue` from '{value}': {e}"),
69 });
70 }
71
72 Ok(Self(Ustr::from(value)))
73 }
74
75 pub fn new<T: AsRef<str>>(value: T) -> Self {
81 Self::new_checked(value).expect_display(FAILED)
82 }
83
84 #[cfg_attr(not(feature = "python"), allow(dead_code))]
86 pub(crate) fn set_inner(&mut self, value: &str) {
87 self.0 = Ustr::from(value);
88 }
89
90 #[must_use]
92 pub fn inner(&self) -> Ustr {
93 self.0
94 }
95
96 #[must_use]
98 pub fn as_str(&self) -> &str {
99 self.0.as_str()
100 }
101
102 #[must_use]
103 pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
104 Self(Ustr::from(s.as_ref()))
105 }
106
107 #[must_use]
108 pub const fn from_ustr_unchecked(s: Ustr) -> Self {
109 Self(s)
110 }
111
112 pub fn from_code(code: &str) -> anyhow::Result<Self> {
116 let map_guard = VENUE_MAP
117 .lock()
118 .map_err(|e| anyhow::anyhow!("Error acquiring lock on `VENUE_MAP`: {e}"))?;
119 map_guard
120 .get(code)
121 .copied()
122 .ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}"))
123 }
124
125 #[must_use]
126 pub fn synthetic() -> Self {
127 Self::new(SYNTHETIC_VENUE)
128 }
129
130 #[must_use]
131 pub fn is_synthetic(&self) -> bool {
132 self.0.as_str() == SYNTHETIC_VENUE
133 }
134
135 #[cfg(feature = "defi")]
137 #[must_use]
138 pub fn is_dex(&self) -> bool {
139 self.0.as_str().contains(':')
140 }
141
142 #[cfg(feature = "defi")]
143 pub fn parse_dex(&self) -> anyhow::Result<(Blockchain, DexType)> {
152 let venue_str = self.as_str();
153
154 if let Some((chain_name, dex_id)) = venue_str.split_once(':') {
155 let chain = Chain::from_chain_name(chain_name).ok_or_else(|| {
157 anyhow::anyhow!("Invalid chain '{chain_name}' in venue '{venue_str}'")
158 })?;
159
160 let dex_type = DexType::from_dex_name(dex_id)
162 .ok_or_else(|| anyhow::anyhow!("Invalid DEX '{dex_id}' in venue '{venue_str}'"))?;
163
164 Ok((chain.name, dex_type))
165 } else {
166 anyhow::bail!("Venue '{venue_str}' is not a DEX venue (expected format 'Chain:DexId')")
167 }
168 }
169}
170
171impl Debug for Venue {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(f, "\"{}\"", self.0)
174 }
175}
176
177impl Display for Venue {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 write!(f, "{}", self.0)
180 }
181}
182
183#[cfg(feature = "defi")]
191pub fn validate_blockchain_venue(venue_part: &str) -> CorrectnessResult<()> {
192 if let Some((chain_name, dex_id)) = venue_part.split_once(':') {
193 if chain_name.is_empty() || dex_id.is_empty() {
194 return Err(CorrectnessError::PredicateViolation {
195 message: format!(
196 "invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'"
197 ),
198 });
199 }
200
201 if Chain::from_chain_name(chain_name).is_none() {
202 return Err(CorrectnessError::PredicateViolation {
203 message: format!(
204 "invalid blockchain venue '{venue_part}': chain '{chain_name}' not recognized"
205 ),
206 });
207 }
208
209 if DexType::from_dex_name(dex_id).is_none() {
210 return Err(CorrectnessError::PredicateViolation {
211 message: format!(
212 "invalid blockchain venue '{venue_part}': dex '{dex_id}' not recognized"
213 ),
214 });
215 }
216 Ok(())
217 } else {
218 Err(CorrectnessError::PredicateViolation {
219 message: format!(
220 "invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'"
221 ),
222 })
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use nautilus_core::correctness::CorrectnessError;
229 use rstest::rstest;
230
231 #[cfg(feature = "defi")]
232 use crate::defi::{Blockchain, DexType};
233 use crate::identifiers::{Venue, stubs::*};
234
235 #[rstest]
236 fn test_string_reprs(venue_binance: Venue) {
237 assert_eq!(venue_binance.as_str(), "BINANCE");
238 assert_eq!(format!("{venue_binance}"), "BINANCE");
239 }
240
241 #[rstest]
242 fn test_new_checked_returns_typed_error_with_stable_display() {
243 let error = Venue::new_checked("").unwrap_err();
244
245 assert_eq!(
246 error,
247 CorrectnessError::EmptyString {
248 param: "value".to_string(),
249 }
250 );
251 assert_eq!(error.to_string(), "invalid string for 'value', was empty");
252 }
253
254 #[cfg(feature = "defi")]
255 #[rstest]
256 #[case(
257 "Arbitrum:",
258 "invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
259 )]
260 #[case(
261 "InvalidChain:UniswapV3",
262 "invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
263 )]
264 #[case(
265 "Arbitrum:InvalidDex",
266 "invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
267 )]
268 #[case(
269 "no-colon",
270 "invalid blockchain venue 'no-colon': expected format 'Chain:DexId'"
271 )]
272 fn test_validate_blockchain_venue_returns_typed_error_with_stable_display(
273 #[case] input: &str,
274 #[case] expected_message: &str,
275 ) {
276 let error = super::validate_blockchain_venue(input).unwrap_err();
277 assert_eq!(
278 error,
279 CorrectnessError::PredicateViolation {
280 message: expected_message.to_string(),
281 }
282 );
283 assert_eq!(error.to_string(), expected_message);
284 }
285
286 #[cfg(feature = "defi")]
287 #[rstest]
288 fn test_blockchain_venue_valid_dex_names() {
289 let valid_dexes = vec![
291 "UniswapV3",
292 "UniswapV2",
293 "UniswapV4",
294 "SushiSwapV2",
295 "SushiSwapV3",
296 "PancakeSwapV3",
297 "CamelotV3",
298 "CurveFinance",
299 "FluidDEX",
300 "MaverickV1",
301 "MaverickV2",
302 "BaseX",
303 "BaseSwapV2",
304 "AerodromeV1",
305 "AerodromeSlipstream",
306 "BalancerV2",
307 "BalancerV3",
308 ];
309
310 for dex_name in valid_dexes {
311 let venue_str = format!("Arbitrum:{dex_name}");
312 let venue = Venue::new(&venue_str);
313 assert_eq!(venue.to_string(), venue_str);
314 }
315 }
316 #[cfg(feature = "defi")]
317 #[rstest]
318 #[should_panic(
319 expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
320 )]
321 fn test_blockchain_venue_invalid_chain() {
322 let _ = Venue::new("InvalidChain:UniswapV3");
323 }
324
325 #[cfg(feature = "defi")]
326 #[rstest]
327 #[should_panic(
328 expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
329 )]
330 fn test_blockchain_venue_empty_dex() {
331 let _ = Venue::new("Arbitrum:");
332 }
333
334 #[cfg(feature = "defi")]
335 #[rstest]
336 fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
337 let venue = Venue::new("Ethereum");
339 assert_eq!(venue.to_string(), "Ethereum");
340 }
341
342 #[cfg(feature = "defi")]
343 #[rstest]
344 #[should_panic(
345 expected = "Error creating `Venue` from 'Arbitrum:InvalidDex': invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
346 )]
347 fn test_blockchain_venue_invalid_dex() {
348 let _ = Venue::new("Arbitrum:InvalidDex");
349 }
350
351 #[cfg(feature = "defi")]
352 #[rstest]
353 #[should_panic(
354 expected = "Error creating `Venue` from 'Arbitrum:uniswapv3': invalid blockchain venue 'Arbitrum:uniswapv3': dex 'uniswapv3' not recognized"
355 )]
356 fn test_blockchain_venue_dex_case_sensitive() {
357 let _ = Venue::new("Arbitrum:uniswapv3");
359 }
360
361 #[cfg(feature = "defi")]
362 #[rstest]
363 fn test_blockchain_venue_various_chain_dex_combinations() {
364 let valid_combinations = vec![
366 ("Ethereum", "UniswapV2"),
367 ("Ethereum", "BalancerV2"),
368 ("Arbitrum", "CamelotV3"),
369 ("Base", "AerodromeV1"),
370 ("Polygon", "SushiSwapV3"),
371 ];
372
373 for (chain, dex) in valid_combinations {
374 let venue_str = format!("{chain}:{dex}");
375 let venue = Venue::new(&venue_str);
376 assert_eq!(venue.to_string(), venue_str);
377 }
378 }
379
380 #[cfg(feature = "defi")]
381 #[rstest]
382 #[case("Ethereum:UniswapV3", Blockchain::Ethereum, DexType::UniswapV3)]
383 #[case("Arbitrum:CamelotV3", Blockchain::Arbitrum, DexType::CamelotV3)]
384 #[case("Base:AerodromeV1", Blockchain::Base, DexType::AerodromeV1)]
385 #[case("Polygon:SushiSwapV2", Blockchain::Polygon, DexType::SushiSwapV2)]
386 fn test_parse_dex_valid(
387 #[case] venue_str: &str,
388 #[case] expected_chain: Blockchain,
389 #[case] expected_dex: DexType,
390 ) {
391 let venue = Venue::new(venue_str);
392 let (blockchain, dex_type) = venue.parse_dex().unwrap();
393
394 assert_eq!(blockchain, expected_chain);
395 assert_eq!(dex_type, expected_dex);
396 }
397
398 #[cfg(feature = "defi")]
399 #[rstest]
400 fn test_parse_dex_non_dex_venue() {
401 let venue = Venue::new("BINANCE");
402 let result = venue.parse_dex();
403 assert!(result.is_err());
404 assert!(
405 result
406 .unwrap_err()
407 .to_string()
408 .contains("is not a DEX venue")
409 );
410 }
411
412 #[cfg(feature = "defi")]
413 #[rstest]
414 fn test_parse_dex_invalid_components() {
415 let venue = Venue::from_str_unchecked("InvalidChain:UniswapV3");
417 assert!(venue.parse_dex().is_err());
418
419 let venue = Venue::from_str_unchecked("Ethereum:InvalidDex");
421 assert!(venue.parse_dex().is_err());
422 }
423}