use ccxt_core::types::symbol::{ExpiryDate, ParsedSymbol, SymbolMarketType};
pub struct BinanceSymbolConverter;
impl BinanceSymbolConverter {
pub fn to_exchange_id(parsed: &ParsedSymbol) -> String {
let base = &parsed.base;
let quote = &parsed.quote;
match parsed.market_type() {
SymbolMarketType::Spot => {
format!("{}{}", base, quote)
}
SymbolMarketType::Swap => {
if parsed.is_inverse() {
format!("{}{}_PERP", base, quote)
} else {
format!("{}{}", base, quote)
}
}
SymbolMarketType::Futures => {
if let Some(ref expiry) = parsed.expiry {
let date_str =
format!("{:02}{:02}{:02}", expiry.year, expiry.month, expiry.day);
format!("{}{}_{}", base, quote, date_str)
} else {
format!("{}{}", base, quote)
}
}
}
}
pub fn from_exchange_id(
exchange_id: &str,
market_type: SymbolMarketType,
settle: Option<&str>,
base: &str,
quote: &str,
) -> Result<ParsedSymbol, String> {
let base = base.to_uppercase();
let quote = quote.to_uppercase();
match market_type {
SymbolMarketType::Spot => Ok(ParsedSymbol::spot(base, quote)),
SymbolMarketType::Swap => {
let settle = settle
.map(str::to_uppercase)
.ok_or_else(|| "Settlement currency required for swap".to_string())?;
Ok(ParsedSymbol::swap(base, quote, settle))
}
SymbolMarketType::Futures => {
let settle = settle
.map(str::to_uppercase)
.ok_or_else(|| "Settlement currency required for futures".to_string())?;
let expiry = Self::extract_expiry_from_exchange_id(exchange_id)?;
Ok(ParsedSymbol::futures(base, quote, settle, expiry))
}
}
}
fn extract_expiry_from_exchange_id(exchange_id: &str) -> Result<ExpiryDate, String> {
if let Some(underscore_pos) = exchange_id.rfind('_') {
let date_part = &exchange_id[underscore_pos + 1..];
if date_part.len() == 6 && date_part.chars().all(|c| c.is_ascii_digit()) {
let year: u8 = date_part[0..2]
.parse()
.map_err(|_| format!("Invalid year in expiry: {}", date_part))?;
let month: u8 = date_part[2..4]
.parse()
.map_err(|_| format!("Invalid month in expiry: {}", date_part))?;
let day: u8 = date_part[4..6]
.parse()
.map_err(|_| format!("Invalid day in expiry: {}", date_part))?;
return ExpiryDate::new(year, month, day)
.map_err(|e| format!("Invalid expiry date: {}", e));
}
}
Err(format!(
"Could not extract expiry date from exchange ID: {}",
exchange_id
))
}
pub fn is_perpetual(exchange_id: &str) -> bool {
exchange_id.ends_with("_PERP")
}
pub fn is_futures(exchange_id: &str) -> bool {
if let Some(underscore_pos) = exchange_id.rfind('_') {
let suffix = &exchange_id[underscore_pos + 1..];
suffix.len() == 6 && suffix.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spot_to_exchange_id() {
let symbol = ParsedSymbol::spot("BTC", "USDT");
assert_eq!(BinanceSymbolConverter::to_exchange_id(&symbol), "BTCUSDT");
}
#[test]
fn test_spot_to_exchange_id_various_pairs() {
let eth_usdt = ParsedSymbol::spot("ETH", "USDT");
assert_eq!(BinanceSymbolConverter::to_exchange_id(ð_usdt), "ETHUSDT");
let btc_busd = ParsedSymbol::spot("BTC", "BUSD");
assert_eq!(BinanceSymbolConverter::to_exchange_id(&btc_busd), "BTCBUSD");
let sol_btc = ParsedSymbol::spot("SOL", "BTC");
assert_eq!(BinanceSymbolConverter::to_exchange_id(&sol_btc), "SOLBTC");
}
#[test]
fn test_linear_swap_to_exchange_id() {
let symbol = ParsedSymbol::linear_swap("BTC", "USDT");
assert_eq!(BinanceSymbolConverter::to_exchange_id(&symbol), "BTCUSDT");
}
#[test]
fn test_inverse_swap_to_exchange_id() {
let symbol = ParsedSymbol::inverse_swap("BTC", "USD");
assert_eq!(
BinanceSymbolConverter::to_exchange_id(&symbol),
"BTCUSD_PERP"
);
}
#[test]
fn test_linear_futures_to_exchange_id() {
let expiry = ExpiryDate::new(24, 12, 31).unwrap();
let symbol = ParsedSymbol::futures("BTC", "USDT", "USDT", expiry);
assert_eq!(
BinanceSymbolConverter::to_exchange_id(&symbol),
"BTCUSDT_241231"
);
}
#[test]
fn test_inverse_futures_to_exchange_id() {
let expiry = ExpiryDate::new(25, 3, 15).unwrap();
let symbol = ParsedSymbol::futures("BTC", "USD", "BTC", expiry);
assert_eq!(
BinanceSymbolConverter::to_exchange_id(&symbol),
"BTCUSD_250315"
);
}
#[test]
fn test_futures_date_padding() {
let expiry = ExpiryDate::new(25, 1, 5).unwrap();
let symbol = ParsedSymbol::futures("ETH", "USDT", "USDT", expiry);
assert_eq!(
BinanceSymbolConverter::to_exchange_id(&symbol),
"ETHUSDT_250105"
);
}
#[test]
fn test_spot_from_exchange_id() {
let parsed = BinanceSymbolConverter::from_exchange_id(
"BTCUSDT",
SymbolMarketType::Spot,
None,
"BTC",
"USDT",
)
.unwrap();
assert_eq!(parsed.base, "BTC");
assert_eq!(parsed.quote, "USDT");
assert!(parsed.settle.is_none());
assert!(parsed.is_spot());
}
#[test]
fn test_swap_from_exchange_id() {
let parsed = BinanceSymbolConverter::from_exchange_id(
"BTCUSDT",
SymbolMarketType::Swap,
Some("USDT"),
"BTC",
"USDT",
)
.unwrap();
assert_eq!(parsed.base, "BTC");
assert_eq!(parsed.quote, "USDT");
assert_eq!(parsed.settle, Some("USDT".to_string()));
assert!(parsed.is_swap());
}
#[test]
fn test_futures_from_exchange_id() {
let parsed = BinanceSymbolConverter::from_exchange_id(
"BTCUSDT_241231",
SymbolMarketType::Futures,
Some("USDT"),
"BTC",
"USDT",
)
.unwrap();
assert_eq!(parsed.base, "BTC");
assert_eq!(parsed.quote, "USDT");
assert_eq!(parsed.settle, Some("USDT".to_string()));
assert!(parsed.is_futures());
let expiry = parsed.expiry.unwrap();
assert_eq!(expiry.year, 24);
assert_eq!(expiry.month, 12);
assert_eq!(expiry.day, 31);
}
#[test]
fn test_is_perpetual() {
assert!(BinanceSymbolConverter::is_perpetual("BTCUSD_PERP"));
assert!(BinanceSymbolConverter::is_perpetual("ETHUSD_PERP"));
assert!(!BinanceSymbolConverter::is_perpetual("BTCUSDT"));
assert!(!BinanceSymbolConverter::is_perpetual("BTCUSDT_241231"));
}
#[test]
fn test_is_futures() {
assert!(BinanceSymbolConverter::is_futures("BTCUSDT_241231"));
assert!(BinanceSymbolConverter::is_futures("ETHUSDT_250315"));
assert!(!BinanceSymbolConverter::is_futures("BTCUSDT"));
assert!(!BinanceSymbolConverter::is_futures("BTCUSD_PERP"));
}
}