use std::{
fmt::{Debug, Display},
hash::Hash,
};
use nautilus_core::correctness::{FAILED, check_valid_string_ascii};
use ustr::Ustr;
#[cfg(feature = "defi")]
use crate::defi::{Blockchain, Chain, DexType};
use crate::venues::VENUE_MAP;
pub const SYNTHETIC_VENUE: &str = "SYNTH";
#[repr(C)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct Venue(Ustr);
impl Venue {
pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
let value = value.as_ref();
check_valid_string_ascii(value, stringify!(value))?;
#[cfg(feature = "defi")]
if value.contains(':')
&& let Err(e) = validate_blockchain_venue(value)
{
anyhow::bail!("Error creating `Venue` from '{value}': {e}");
}
Ok(Self(Ustr::from(value)))
}
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self::new_checked(value).expect(FAILED)
}
#[cfg_attr(not(feature = "python"), allow(dead_code))]
pub(crate) fn set_inner(&mut self, value: &str) {
self.0 = Ustr::from(value);
}
#[must_use]
pub fn inner(&self) -> Ustr {
self.0
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
Self(Ustr::from(s.as_ref()))
}
#[must_use]
pub const fn from_ustr_unchecked(s: Ustr) -> Self {
Self(s)
}
pub fn from_code(code: &str) -> anyhow::Result<Self> {
let map_guard = VENUE_MAP
.lock()
.map_err(|e| anyhow::anyhow!("Error acquiring lock on `VENUE_MAP`: {e}"))?;
map_guard
.get(code)
.copied()
.ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}"))
}
#[must_use]
pub fn synthetic() -> Self {
Self::new(SYNTHETIC_VENUE)
}
#[must_use]
pub fn is_synthetic(&self) -> bool {
self.0.as_str() == SYNTHETIC_VENUE
}
#[cfg(feature = "defi")]
#[must_use]
pub fn is_dex(&self) -> bool {
self.0.as_str().contains(':')
}
#[cfg(feature = "defi")]
pub fn parse_dex(&self) -> anyhow::Result<(Blockchain, DexType)> {
let venue_str = self.as_str();
if let Some((chain_name, dex_id)) = venue_str.split_once(':') {
let chain = Chain::from_chain_name(chain_name).ok_or_else(|| {
anyhow::anyhow!("Invalid chain '{chain_name}' in venue '{venue_str}'")
})?;
let dex_type = DexType::from_dex_name(dex_id)
.ok_or_else(|| anyhow::anyhow!("Invalid DEX '{dex_id}' in venue '{venue_str}'"))?;
Ok((chain.name, dex_type))
} else {
anyhow::bail!("Venue '{venue_str}' is not a DEX venue (expected format 'Chain:DexId')")
}
}
}
impl Debug for Venue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
impl Display for Venue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "defi")]
pub fn validate_blockchain_venue(venue_part: &str) -> anyhow::Result<()> {
if let Some((chain_name, dex_id)) = venue_part.split_once(':') {
if chain_name.is_empty() || dex_id.is_empty() {
anyhow::bail!("invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'");
}
if Chain::from_chain_name(chain_name).is_none() {
anyhow::bail!(
"invalid blockchain venue '{venue_part}': chain '{chain_name}' not recognized"
);
}
if DexType::from_dex_name(dex_id).is_none() {
anyhow::bail!("invalid blockchain venue '{venue_part}': dex '{dex_id}' not recognized");
}
Ok(())
} else {
anyhow::bail!("invalid blockchain venue '{venue_part}': expected format 'Chain:DexId'");
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[cfg(feature = "defi")]
use crate::defi::{Blockchain, DexType};
use crate::identifiers::{Venue, stubs::*};
#[rstest]
fn test_string_reprs(venue_binance: Venue) {
assert_eq!(venue_binance.as_str(), "BINANCE");
assert_eq!(format!("{venue_binance}"), "BINANCE");
}
#[cfg(feature = "defi")]
#[rstest]
fn test_blockchain_venue_valid_dex_names() {
let valid_dexes = vec![
"UniswapV3",
"UniswapV2",
"UniswapV4",
"SushiSwapV2",
"SushiSwapV3",
"PancakeSwapV3",
"CamelotV3",
"CurveFinance",
"FluidDEX",
"MaverickV1",
"MaverickV2",
"BaseX",
"BaseSwapV2",
"AerodromeV1",
"AerodromeSlipstream",
"BalancerV2",
"BalancerV3",
];
for dex_name in valid_dexes {
let venue_str = format!("Arbitrum:{dex_name}");
let venue = Venue::new(&venue_str);
assert_eq!(venue.to_string(), venue_str);
}
}
#[cfg(feature = "defi")]
#[rstest]
#[should_panic(
expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
)]
fn test_blockchain_venue_invalid_chain() {
let _ = Venue::new("InvalidChain:UniswapV3");
}
#[cfg(feature = "defi")]
#[rstest]
#[should_panic(
expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
)]
fn test_blockchain_venue_empty_dex() {
let _ = Venue::new("Arbitrum:");
}
#[cfg(feature = "defi")]
#[rstest]
fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
let venue = Venue::new("Ethereum");
assert_eq!(venue.to_string(), "Ethereum");
}
#[cfg(feature = "defi")]
#[rstest]
#[should_panic(
expected = "Error creating `Venue` from 'Arbitrum:InvalidDex': invalid blockchain venue 'Arbitrum:InvalidDex': dex 'InvalidDex' not recognized"
)]
fn test_blockchain_venue_invalid_dex() {
let _ = Venue::new("Arbitrum:InvalidDex");
}
#[cfg(feature = "defi")]
#[rstest]
#[should_panic(
expected = "Error creating `Venue` from 'Arbitrum:uniswapv3': invalid blockchain venue 'Arbitrum:uniswapv3': dex 'uniswapv3' not recognized"
)]
fn test_blockchain_venue_dex_case_sensitive() {
let _ = Venue::new("Arbitrum:uniswapv3");
}
#[cfg(feature = "defi")]
#[rstest]
fn test_blockchain_venue_various_chain_dex_combinations() {
let valid_combinations = vec![
("Ethereum", "UniswapV2"),
("Ethereum", "BalancerV2"),
("Arbitrum", "CamelotV3"),
("Base", "AerodromeV1"),
("Polygon", "SushiSwapV3"),
];
for (chain, dex) in valid_combinations {
let venue_str = format!("{chain}:{dex}");
let venue = Venue::new(&venue_str);
assert_eq!(venue.to_string(), venue_str);
}
}
#[cfg(feature = "defi")]
#[rstest]
#[case("Ethereum:UniswapV3", Blockchain::Ethereum, DexType::UniswapV3)]
#[case("Arbitrum:CamelotV3", Blockchain::Arbitrum, DexType::CamelotV3)]
#[case("Base:AerodromeV1", Blockchain::Base, DexType::AerodromeV1)]
#[case("Polygon:SushiSwapV2", Blockchain::Polygon, DexType::SushiSwapV2)]
fn test_parse_dex_valid(
#[case] venue_str: &str,
#[case] expected_chain: Blockchain,
#[case] expected_dex: DexType,
) {
let venue = Venue::new(venue_str);
let (blockchain, dex_type) = venue.parse_dex().unwrap();
assert_eq!(blockchain, expected_chain);
assert_eq!(dex_type, expected_dex);
}
#[cfg(feature = "defi")]
#[rstest]
fn test_parse_dex_non_dex_venue() {
let venue = Venue::new("BINANCE");
let result = venue.parse_dex();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("is not a DEX venue")
);
}
#[cfg(feature = "defi")]
#[rstest]
fn test_parse_dex_invalid_components() {
let venue = Venue::from_str_unchecked("InvalidChain:UniswapV3");
assert!(venue.parse_dex().is_err());
let venue = Venue::from_str_unchecked("Ethereum:InvalidDex");
assert!(venue.parse_dex().is_err());
}
}