use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::crypto_classifier::{is_crypto_base, is_crypto_quote};
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TickerError {
#[error("invalid ticker format '{0}': expected BASE/QUOTE (e.g., BTC/USD)")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetClass {
Equity,
Crypto,
Commodity,
Forex,
}
impl FromStr for AssetClass {
type Err = TickerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"equity" | "stock" => Ok(AssetClass::Equity),
"crypto" | "cryptocurrency" => Ok(AssetClass::Crypto),
"commodity" => Ok(AssetClass::Commodity),
"forex" | "fx" => Ok(AssetClass::Forex),
_ => Err(TickerError::InvalidFormat(format!(
"unknown asset class: '{s}'"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Underlying {
canonical: Arc<str>,
asset_class: AssetClass,
}
impl Underlying {
pub fn new(canonical: impl Into<String>, asset_class: AssetClass) -> Self {
Self {
canonical: Arc::from(canonical.into().to_uppercase().as_str()),
asset_class,
}
}
pub fn canonical(&self) -> &str {
&self.canonical
}
pub fn asset_class(&self) -> AssetClass {
self.asset_class
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Derivative {
Perp,
Cfd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetType {
Spot,
Derivative(Derivative),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VenueType {
TradFi,
Cex,
Dex,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Ticker {
underlying: Underlying,
quote: Arc<str>,
asset_type: AssetType,
venue_type: VenueType,
#[serde(default, skip_serializing_if = "Option::is_none")]
exchange_hint: Option<Arc<str>>,
}
impl Ticker {
pub fn new(
underlying: Underlying,
quote: impl Into<String>,
asset_type: AssetType,
venue_type: VenueType,
) -> Self {
Self {
underlying,
quote: Arc::from(quote.into().to_uppercase().as_str()),
asset_type,
venue_type,
exchange_hint: None,
}
}
pub fn with_exchange_hint(mut self, hint: impl Into<String>) -> Self {
self.exchange_hint = Some(Arc::from(hint.into().to_uppercase().as_str()));
self
}
pub fn underlying(&self) -> &Underlying {
&self.underlying
}
pub fn quote(&self) -> &str {
&self.quote
}
pub fn asset_type(&self) -> AssetType {
self.asset_type
}
pub fn venue_type(&self) -> VenueType {
self.venue_type
}
pub fn exchange_hint(&self) -> Option<&str> {
self.exchange_hint.as_deref()
}
pub fn canonical(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Ticker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.underlying.canonical(), self.quote)
}
}
impl FromStr for Ticker {
type Err = TickerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((base, quote)) = s.split_once('/') else {
return Err(TickerError::InvalidFormat(s.to_string()));
};
if base.is_empty() || quote.is_empty() {
return Err(TickerError::InvalidFormat(s.to_string()));
}
if quote.contains('/') {
return Err(TickerError::InvalidFormat(s.to_string()));
}
let asset_class = if is_crypto_quote(quote) || is_crypto_base(base) {
AssetClass::Crypto
} else {
AssetClass::Equity
};
let venue_type = match asset_class {
AssetClass::Crypto => VenueType::Cex,
AssetClass::Equity => VenueType::TradFi,
_ => VenueType::TradFi,
};
let underlying = Underlying::new(base, asset_class);
Ok(Ticker::new(underlying, quote, AssetType::Spot, venue_type))
}
}
use crate::Symbol;
impl From<&Symbol> for Ticker {
fn from(symbol: &Symbol) -> Self {
let (asset_class, venue_type) = if symbol.is_crypto() {
(AssetClass::Crypto, VenueType::Cex)
} else {
(AssetClass::Equity, VenueType::TradFi)
};
let underlying = Underlying::new(symbol.base(), asset_class);
Ticker::new(underlying, symbol.quote(), AssetType::Spot, venue_type)
}
}
#[cfg(test)]
#[path = "taxonomy_tests.rs"]
mod tests;