use super::*;
#[test]
fn underlying_stores_canonical_uppercase() {
let u = Underlying::new("btc", AssetClass::Crypto);
assert_eq!(u.canonical(), "BTC");
}
#[test]
fn underlying_stores_asset_class() {
let u = Underlying::new("TSLA", AssetClass::Equity);
assert_eq!(u.asset_class(), AssetClass::Equity);
}
#[test]
fn ticker_stores_underlying() {
let u = Underlying::new("BTC", AssetClass::Crypto);
let t = Ticker::new(u.clone(), "USDT", AssetType::Spot, VenueType::Cex);
assert_eq!(t.underlying().canonical(), "BTC");
}
#[test]
fn ticker_normalizes_quote_uppercase() {
let u = Underlying::new("BTC", AssetClass::Crypto);
let t = Ticker::new(u, "usdt", AssetType::Spot, VenueType::Cex);
assert_eq!(t.quote(), "USDT");
}
#[test]
fn ticker_stores_asset_type_spot() {
let u = Underlying::new("BTC", AssetClass::Crypto);
let t = Ticker::new(u, "USDT", AssetType::Spot, VenueType::Cex);
assert_eq!(t.asset_type(), AssetType::Spot);
}
#[test]
fn ticker_stores_asset_type_perp() {
let u = Underlying::new("SOL", AssetClass::Crypto);
let t = Ticker::new(
u,
"USDC",
AssetType::Derivative(Derivative::Perp),
VenueType::Dex,
);
assert_eq!(t.asset_type(), AssetType::Derivative(Derivative::Perp));
}
#[test]
fn ticker_stores_venue_type() {
let u = Underlying::new("TSLA", AssetClass::Equity);
let t = Ticker::new(u, "USD", AssetType::Spot, VenueType::TradFi);
assert_eq!(t.venue_type(), VenueType::TradFi);
}
#[test]
fn ticker_canonical_format() {
let u = Underlying::new("SOL", AssetClass::Crypto);
let t = Ticker::new(u, "USDC", AssetType::Spot, VenueType::Dex);
assert_eq!(t.canonical(), "SOL/USDC");
}
#[test]
fn same_underlying_across_venues() {
let u = Underlying::new("TSLA", AssetClass::Equity);
let tradfi = Ticker::new(u.clone(), "USD", AssetType::Spot, VenueType::TradFi);
let dex = Ticker::new(u, "USDC", AssetType::Spot, VenueType::Dex);
assert_eq!(
tradfi.underlying().canonical(),
dex.underlying().canonical()
);
}
#[test]
fn from_str_valid_ticker() {
let t: Ticker = "BTC/USD".parse().expect("valid ticker");
assert_eq!(t.underlying().canonical(), "BTC");
assert_eq!(t.quote(), "USD");
assert_eq!(t.asset_type(), AssetType::Spot);
assert_eq!(t.venue_type(), VenueType::Cex);
}
#[test]
fn from_str_normalizes_case() {
let t: Ticker = "eth/usdt".parse().expect("valid ticker");
assert_eq!(t.underlying().canonical(), "ETH");
assert_eq!(t.quote(), "USDT");
}
#[test]
fn from_str_rejects_no_separator() {
let result = "BTCUSD".parse::<Ticker>();
assert!(matches!(result, Err(TickerError::InvalidFormat(s)) if s == "BTCUSD"));
}
#[test]
fn from_str_rejects_too_many_parts() {
let result = "BTC/USD/EXTRA".parse::<Ticker>();
assert!(matches!(result, Err(TickerError::InvalidFormat(_))));
}
#[test]
fn from_str_rejects_empty() {
let result = "".parse::<Ticker>();
assert!(matches!(result, Err(TickerError::InvalidFormat(_))));
}
#[test]
fn from_str_rejects_empty_base() {
let result = "/USD".parse::<Ticker>();
assert!(matches!(result, Err(TickerError::InvalidFormat(_))));
}
#[test]
fn from_str_rejects_empty_quote() {
let result = "BTC/".parse::<Ticker>();
assert!(matches!(result, Err(TickerError::InvalidFormat(_))));
}
#[test]
fn display_matches_canonical() {
let u = Underlying::new("BTC", AssetClass::Crypto);
let t = Ticker::new(u, "USD", AssetType::Spot, VenueType::Cex);
assert_eq!(format!("{t}"), "BTC/USD");
assert_eq!(t.to_string(), t.canonical());
}
#[test]
fn canonical_roundtrip_through_parse() {
let u = Underlying::new("SOL", AssetClass::Crypto);
let original = Ticker::new(u, "USDC", AssetType::Spot, VenueType::Cex);
let canonical = original.canonical();
let parsed: Ticker = canonical.parse().expect("valid ticker");
assert_eq!(parsed.underlying().canonical(), "SOL");
assert_eq!(parsed.quote(), "USDC");
}
#[test]
fn from_str_infers_crypto_from_crypto_quote() {
let t: Ticker = "BTC/USDT".parse().expect("valid ticker");
assert_eq!(t.underlying().asset_class(), AssetClass::Crypto);
assert_eq!(t.venue_type(), VenueType::Cex);
}
#[test]
fn from_str_infers_crypto_from_crypto_base_with_usd() {
let t: Ticker = "BTC/USD".parse().expect("valid ticker");
assert_eq!(t.underlying().asset_class(), AssetClass::Crypto);
assert_eq!(t.venue_type(), VenueType::Cex);
let t2: Ticker = "ETH/USD".parse().expect("valid ticker");
assert_eq!(t2.underlying().asset_class(), AssetClass::Crypto);
let t3: Ticker = "SOL/USD".parse().expect("valid ticker");
assert_eq!(t3.underlying().asset_class(), AssetClass::Crypto);
}
#[test]
fn from_str_infers_equity_from_non_crypto_base_with_usd() {
let t: Ticker = "AAPL/USD".parse().expect("valid ticker");
assert_eq!(t.underlying().asset_class(), AssetClass::Equity);
assert_eq!(t.venue_type(), VenueType::TradFi);
let t2: Ticker = "TSLA/USD".parse().expect("valid ticker");
assert_eq!(t2.underlying().asset_class(), AssetClass::Equity);
let t3: Ticker = "MSFT/USD".parse().expect("valid ticker");
assert_eq!(t3.underlying().asset_class(), AssetClass::Equity);
}
#[test]
fn asset_class_from_str_all_variants() {
assert_eq!(
"equity".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Equity
);
assert_eq!(
"crypto".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Crypto
);
assert_eq!(
"commodity"
.parse::<AssetClass>()
.expect("valid asset class"),
AssetClass::Commodity
);
assert_eq!(
"forex".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Forex
);
}
#[test]
fn asset_class_from_str_aliases() {
assert_eq!(
"stock".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Equity
);
assert_eq!(
"cryptocurrency"
.parse::<AssetClass>()
.expect("valid asset class"),
AssetClass::Crypto
);
assert_eq!(
"fx".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Forex
);
}
#[test]
fn asset_class_from_str_case_insensitive() {
assert_eq!(
"EQUITY".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Equity
);
assert_eq!(
"Crypto".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Crypto
);
assert_eq!(
"FOREX".parse::<AssetClass>().expect("valid asset class"),
AssetClass::Forex
);
}
#[test]
fn asset_class_from_str_rejects_unknown() {
let result = "bonds".parse::<AssetClass>();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("bonds"),
"error should contain the invalid input"
);
}
#[test]
fn asset_class_serde_roundtrip() {
for variant in [
AssetClass::Equity,
AssetClass::Crypto,
AssetClass::Commodity,
AssetClass::Forex,
] {
let json = serde_json::to_string(&variant).expect("valid json");
let parsed: AssetClass = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, variant);
}
}
#[test]
fn derivative_variants_are_distinct() {
assert_ne!(Derivative::Perp, Derivative::Cfd);
}
#[test]
fn derivative_serde_roundtrip() {
for variant in [Derivative::Perp, Derivative::Cfd] {
let json = serde_json::to_string(&variant).expect("valid json");
let parsed: Derivative = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, variant);
}
}
#[test]
fn derivative_debug_format() {
assert_eq!(format!("{:?}", Derivative::Perp), "Perp");
assert_eq!(format!("{:?}", Derivative::Cfd), "Cfd");
}
#[test]
fn asset_type_spot_vs_derivative() {
assert_ne!(AssetType::Spot, AssetType::Derivative(Derivative::Perp));
assert_ne!(AssetType::Spot, AssetType::Derivative(Derivative::Cfd));
}
#[test]
fn asset_type_derivative_variants_are_distinct() {
assert_ne!(
AssetType::Derivative(Derivative::Perp),
AssetType::Derivative(Derivative::Cfd)
);
}
#[test]
fn asset_type_serde_roundtrip() {
for variant in [
AssetType::Spot,
AssetType::Derivative(Derivative::Perp),
AssetType::Derivative(Derivative::Cfd),
] {
let json = serde_json::to_string(&variant).expect("valid json");
let parsed: AssetType = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, variant);
}
}
#[test]
fn venue_type_all_variants_distinct() {
let variants = [VenueType::TradFi, VenueType::Cex, VenueType::Dex];
for (i, a) in variants.iter().enumerate() {
for (j, b) in variants.iter().enumerate() {
if i != j {
assert_ne!(a, b);
}
}
}
}
#[test]
fn venue_type_serde_roundtrip() {
for variant in [VenueType::TradFi, VenueType::Cex, VenueType::Dex] {
let json = serde_json::to_string(&variant).expect("valid json");
let parsed: VenueType = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, variant);
}
}
#[test]
fn venue_type_debug_format() {
assert_eq!(format!("{:?}", VenueType::TradFi), "TradFi");
assert_eq!(format!("{:?}", VenueType::Cex), "Cex");
assert_eq!(format!("{:?}", VenueType::Dex), "Dex");
}
#[test]
fn underlying_serde_roundtrip() {
let original = Underlying::new("BTC", AssetClass::Crypto);
let json = serde_json::to_string(&original).expect("valid json");
let parsed: Underlying = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed.canonical(), "BTC");
assert_eq!(parsed.asset_class(), AssetClass::Crypto);
}
#[test]
fn underlying_equality() {
let a = Underlying::new("btc", AssetClass::Crypto);
let b = Underlying::new("BTC", AssetClass::Crypto);
assert_eq!(a, b, "underlying normalizes to uppercase");
}
#[test]
fn ticker_serde_roundtrip() {
let u = Underlying::new("SOL", AssetClass::Crypto);
let original = Ticker::new(
u,
"USDC",
AssetType::Derivative(Derivative::Perp),
VenueType::Dex,
);
let json = serde_json::to_string(&original).expect("valid json");
let parsed: Ticker = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, original);
}
#[test]
fn ticker_error_display_contains_input() {
let err = TickerError::InvalidFormat("bad_input".to_string());
let msg = err.to_string();
assert!(
msg.contains("bad_input"),
"error message should contain the invalid input"
);
}