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.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.lock();
117 map_guard
118 .get(code)
119 .copied()
120 .ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}"))
121 }
122
123 #[must_use]
124 pub fn synthetic() -> Self {
125 Self::new(SYNTHETIC_VENUE)
126 }
127
128 #[must_use]
129 pub fn is_synthetic(&self) -> bool {
130 self.0 == SYNTHETIC_VENUE
131 }
132
133 #[cfg(feature = "defi")]
135 #[must_use]
136 pub fn is_dex(&self) -> bool {
137 self.0.as_str().contains(':')
138 }
139
140 #[cfg(feature = "defi")]
141 pub fn parse_dex(&self) -> anyhow::Result<(Blockchain, DexType)> {
150 let venue_str = self.as_str();
151 let Some((chain_name, dex_id)) = venue_str.split_once(':') else {
152 anyhow::bail!("Venue '{venue_str}' is not a DEX venue (expected format 'Chain:DexId')")
153 };
154
155 let chain = Chain::from_chain_name(chain_name).ok_or_else(|| {
156 anyhow::anyhow!("Invalid chain '{chain_name}' in venue '{venue_str}'")
157 })?;
158 let dex_type = DexType::from_dex_name(dex_id)
159 .ok_or_else(|| anyhow::anyhow!("Invalid DEX '{dex_id}' in venue '{venue_str}'"))?;
160
161 Ok((chain.name, dex_type))
162 }
163}
164
165impl Debug for Venue {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 write!(f, "\"{}\"", self.0)
168 }
169}
170
171impl Display for Venue {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(f, "{}", self.0)
174 }
175}
176
177#[cfg(feature = "defi")]
185pub fn validate_blockchain_venue(venue_part: &str) -> CorrectnessResult<()> {
186 let invalid_format = || CorrectnessError::PredicateViolation {
187 message: format!("invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'"),
188 };
189
190 let Some((chain_name, dex_id)) = venue_part.split_once(':') else {
191 return Err(invalid_format());
192 };
193
194 if chain_name.is_empty() || dex_id.is_empty() {
195 return Err(invalid_format());
196 }
197
198 if Chain::from_chain_name(chain_name).is_none() {
199 return Err(CorrectnessError::PredicateViolation {
200 message: format!(
201 "invalid blockchain venue '{venue_part}': chain '{chain_name}' not recognized"
202 ),
203 });
204 }
205
206 if DexType::from_dex_name(dex_id).is_none() {
207 return Err(CorrectnessError::PredicateViolation {
208 message: format!(
209 "invalid blockchain venue '{venue_part}': dex '{dex_id}' not recognized"
210 ),
211 });
212 }
213
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use nautilus_core::correctness::CorrectnessError;
220 use rstest::rstest;
221
222 #[cfg(feature = "defi")]
223 use crate::defi::{Blockchain, DexType};
224 use crate::identifiers::{Venue, stubs::*};
225
226 #[rstest]
227 fn test_string_reprs(venue_binance: Venue) {
228 assert_eq!(venue_binance.as_str(), "BINANCE");
229 assert_eq!(format!("{venue_binance}"), "BINANCE");
230 }
231
232 #[rstest]
233 fn test_new_checked_returns_typed_error_with_stable_display() {
234 let error = Venue::new_checked("").unwrap_err();
235
236 assert_eq!(
237 error,
238 CorrectnessError::EmptyString {
239 param: "value".to_string(),
240 }
241 );
242 assert_eq!(error.to_string(), "invalid string for 'value', was empty");
243 }
244
245 #[rstest]
246 fn test_from_code_returns_mapped_venue() {
247 assert_eq!(Venue::from_code("XCME").unwrap(), Venue::XCME());
248 }
249
250 #[rstest]
251 fn test_from_code_rejects_unknown_code() {
252 let error = Venue::from_code("UNKNOWN").unwrap_err();
253 assert_eq!(error.to_string(), "Unknown venue code: UNKNOWN");
254 }
255
256 #[cfg(feature = "defi")]
257 #[rstest]
258 #[case(
259 "Arbitrum:",
260 "invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
261 )]
262 #[case(
263 "InvalidChain:UniswapV3",
264 "invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
265 )]
266 #[case(
267 "Arbitrum:InvalidDex",
268 "invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
269 )]
270 #[case(
271 "no-colon",
272 "invalid blockchain venue 'no-colon': expected format 'Chain:DexId'"
273 )]
274 fn test_validate_blockchain_venue_returns_typed_error_with_stable_display(
275 #[case] input: &str,
276 #[case] expected_message: &str,
277 ) {
278 let error = super::validate_blockchain_venue(input).unwrap_err();
279 assert_eq!(
280 error,
281 CorrectnessError::PredicateViolation {
282 message: expected_message.to_string(),
283 }
284 );
285 assert_eq!(error.to_string(), expected_message);
286 }
287
288 #[cfg(feature = "defi")]
289 #[rstest]
290 fn test_blockchain_venue_valid_dex_names() {
291 let valid_dexes = [
292 "UniswapV3",
293 "UniswapV2",
294 "UniswapV4",
295 "SushiSwapV2",
296 "SushiSwapV3",
297 "PancakeSwapV3",
298 "CamelotV3",
299 "CurveFinance",
300 "FluidDEX",
301 "MaverickV1",
302 "MaverickV2",
303 "BaseX",
304 "BaseSwapV2",
305 "AerodromeV1",
306 "AerodromeSlipstream",
307 "BalancerV2",
308 "BalancerV3",
309 ];
310
311 for dex_name in valid_dexes {
312 let venue_str = format!("Arbitrum:{dex_name}");
313 let venue = Venue::new(&venue_str);
314 assert_eq!(venue.to_string(), venue_str);
315 }
316 }
317 #[cfg(feature = "defi")]
318 #[rstest]
319 #[should_panic(
320 expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
321 )]
322 fn test_blockchain_venue_invalid_chain() {
323 let _ = Venue::new("InvalidChain:UniswapV3");
324 }
325
326 #[cfg(feature = "defi")]
327 #[rstest]
328 #[should_panic(
329 expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
330 )]
331 fn test_blockchain_venue_empty_dex() {
332 let _ = Venue::new("Arbitrum:");
333 }
334
335 #[cfg(feature = "defi")]
336 #[rstest]
337 fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
338 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");
358 }
359
360 #[cfg(feature = "defi")]
361 #[rstest]
362 fn test_blockchain_venue_various_chain_dex_combinations() {
363 let valid_combinations = [
364 ("Ethereum", "UniswapV2"),
365 ("Ethereum", "BalancerV2"),
366 ("Arbitrum", "CamelotV3"),
367 ("Base", "AerodromeV1"),
368 ("Polygon", "SushiSwapV3"),
369 ];
370
371 for (chain, dex) in valid_combinations {
372 let venue_str = format!("{chain}:{dex}");
373 let venue = Venue::new(&venue_str);
374 assert_eq!(venue.to_string(), venue_str);
375 }
376 }
377
378 #[cfg(feature = "defi")]
379 #[rstest]
380 #[case("Ethereum:UniswapV3", Blockchain::Ethereum, DexType::UniswapV3)]
381 #[case("Arbitrum:CamelotV3", Blockchain::Arbitrum, DexType::CamelotV3)]
382 #[case("Base:AerodromeV1", Blockchain::Base, DexType::AerodromeV1)]
383 #[case("Polygon:SushiSwapV2", Blockchain::Polygon, DexType::SushiSwapV2)]
384 fn test_parse_dex_valid(
385 #[case] venue_str: &str,
386 #[case] expected_chain: Blockchain,
387 #[case] expected_dex: DexType,
388 ) {
389 let venue = Venue::new(venue_str);
390 let (blockchain, dex_type) = venue.parse_dex().unwrap();
391
392 assert_eq!(blockchain, expected_chain);
393 assert_eq!(dex_type, expected_dex);
394 }
395
396 #[cfg(feature = "defi")]
397 #[rstest]
398 fn test_parse_dex_non_dex_venue() {
399 let venue = Venue::new("BINANCE");
400 assert!(
401 venue
402 .parse_dex()
403 .unwrap_err()
404 .to_string()
405 .contains("is not a DEX venue")
406 );
407 }
408
409 #[cfg(feature = "defi")]
410 #[rstest]
411 #[case("InvalidChain:UniswapV3")]
412 #[case("Ethereum:InvalidDex")]
413 fn test_parse_dex_invalid_component(#[case] value: &str) {
414 let venue = Venue::from_str_unchecked(value);
415 assert!(venue.parse_dex().is_err());
416 }
417}