nautilus_model/identifiers/
venue.rs1use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21};
22
23use nautilus_core::correctness::{FAILED, check_valid_string_ascii};
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_ascii(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 #[cfg_attr(not(feature = "python"), 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 '{chain_name}' in venue '{venue_str}'")
149 })?;
150
151 let dex_type = DexType::from_dex_name(dex_id)
153 .ok_or_else(|| anyhow::anyhow!("Invalid DEX '{dex_id}' in venue '{venue_str}'"))?;
154
155 Ok((chain.name, dex_type))
156 } else {
157 anyhow::bail!("Venue '{venue_str}' is not a DEX venue (expected format 'Chain:DexId')")
158 }
159 }
160}
161
162impl Debug for Venue {
163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164 write!(f, "{:?}", self.0)
165 }
166}
167
168impl Display for Venue {
169 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170 write!(f, "{}", self.0)
171 }
172}
173
174#[cfg(feature = "defi")]
182pub fn validate_blockchain_venue(venue_part: &str) -> anyhow::Result<()> {
183 if let Some((chain_name, dex_id)) = venue_part.split_once(':') {
184 if chain_name.is_empty() || dex_id.is_empty() {
185 anyhow::bail!("invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'");
186 }
187 if Chain::from_chain_name(chain_name).is_none() {
188 anyhow::bail!(
189 "invalid blockchain venue '{venue_part}': chain '{chain_name}' not recognized"
190 );
191 }
192 if DexType::from_dex_name(dex_id).is_none() {
193 anyhow::bail!("invalid blockchain venue '{venue_part}': dex '{dex_id}' not recognized");
194 }
195 Ok(())
196 } else {
197 anyhow::bail!("invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'");
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use rstest::rstest;
204
205 #[cfg(feature = "defi")]
206 use crate::defi::{Blockchain, DexType};
207 use crate::identifiers::{Venue, stubs::*};
208
209 #[rstest]
210 fn test_string_reprs(venue_binance: Venue) {
211 assert_eq!(venue_binance.as_str(), "BINANCE");
212 assert_eq!(format!("{venue_binance}"), "BINANCE");
213 }
214
215 #[cfg(feature = "defi")]
216 #[rstest]
217 fn test_blockchain_venue_valid_dex_names() {
218 let valid_dexes = vec![
220 "UniswapV3",
221 "UniswapV2",
222 "UniswapV4",
223 "SushiSwapV2",
224 "SushiSwapV3",
225 "PancakeSwapV3",
226 "CamelotV3",
227 "CurveFinance",
228 "FluidDEX",
229 "MaverickV1",
230 "MaverickV2",
231 "BaseX",
232 "BaseSwapV2",
233 "AerodromeV1",
234 "AerodromeSlipstream",
235 "BalancerV2",
236 "BalancerV3",
237 ];
238
239 for dex_name in valid_dexes {
240 let venue_str = format!("Arbitrum:{dex_name}");
241 let venue = Venue::new(&venue_str);
242 assert_eq!(venue.to_string(), venue_str);
243 }
244 }
245 #[cfg(feature = "defi")]
246 #[rstest]
247 #[should_panic(
248 expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
249 )]
250 fn test_blockchain_venue_invalid_chain() {
251 let _ = Venue::new("InvalidChain:UniswapV3");
252 }
253
254 #[cfg(feature = "defi")]
255 #[rstest]
256 #[should_panic(
257 expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
258 )]
259 fn test_blockchain_venue_empty_dex() {
260 let _ = Venue::new("Arbitrum:");
261 }
262
263 #[cfg(feature = "defi")]
264 #[rstest]
265 fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
266 let venue = Venue::new("Ethereum");
268 assert_eq!(venue.to_string(), "Ethereum");
269 }
270
271 #[cfg(feature = "defi")]
272 #[rstest]
273 #[should_panic(
274 expected = "Error creating `Venue` from 'Arbitrum:InvalidDex': invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
275 )]
276 fn test_blockchain_venue_invalid_dex() {
277 let _ = Venue::new("Arbitrum:InvalidDex");
278 }
279
280 #[cfg(feature = "defi")]
281 #[rstest]
282 #[should_panic(
283 expected = "Error creating `Venue` from 'Arbitrum:uniswapv3': invalid blockchain venue 'Arbitrum:uniswapv3': dex 'uniswapv3' not recognized"
284 )]
285 fn test_blockchain_venue_dex_case_sensitive() {
286 let _ = Venue::new("Arbitrum:uniswapv3");
288 }
289
290 #[cfg(feature = "defi")]
291 #[rstest]
292 fn test_blockchain_venue_various_chain_dex_combinations() {
293 let valid_combinations = vec![
295 ("Ethereum", "UniswapV2"),
296 ("Ethereum", "BalancerV2"),
297 ("Arbitrum", "CamelotV3"),
298 ("Base", "AerodromeV1"),
299 ("Polygon", "SushiSwapV3"),
300 ];
301
302 for (chain, dex) in valid_combinations {
303 let venue_str = format!("{chain}:{dex}");
304 let venue = Venue::new(&venue_str);
305 assert_eq!(venue.to_string(), venue_str);
306 }
307 }
308
309 #[cfg(feature = "defi")]
310 #[rstest]
311 #[case("Ethereum:UniswapV3", Blockchain::Ethereum, DexType::UniswapV3)]
312 #[case("Arbitrum:CamelotV3", Blockchain::Arbitrum, DexType::CamelotV3)]
313 #[case("Base:AerodromeV1", Blockchain::Base, DexType::AerodromeV1)]
314 #[case("Polygon:SushiSwapV2", Blockchain::Polygon, DexType::SushiSwapV2)]
315 fn test_parse_dex_valid(
316 #[case] venue_str: &str,
317 #[case] expected_chain: Blockchain,
318 #[case] expected_dex: DexType,
319 ) {
320 let venue = Venue::new(venue_str);
321 let (blockchain, dex_type) = venue.parse_dex().unwrap();
322
323 assert_eq!(blockchain, expected_chain);
324 assert_eq!(dex_type, expected_dex);
325 }
326
327 #[cfg(feature = "defi")]
328 #[rstest]
329 fn test_parse_dex_non_dex_venue() {
330 let venue = Venue::new("BINANCE");
331 let result = venue.parse_dex();
332 assert!(result.is_err());
333 assert!(
334 result
335 .unwrap_err()
336 .to_string()
337 .contains("is not a DEX venue")
338 );
339 }
340
341 #[cfg(feature = "defi")]
342 #[rstest]
343 fn test_parse_dex_invalid_components() {
344 let venue = Venue::from_str_unchecked("InvalidChain:UniswapV3");
346 assert!(venue.parse_dex().is_err());
347
348 let venue = Venue::from_str_unchecked("Ethereum:InvalidDex");
350 assert!(venue.parse_dex().is_err());
351 }
352}