use ccxt_core::types::symbol::{ExpiryDate, ParsedSymbol, SymbolMarketType};
pub struct OkxSymbolConverter;
impl OkxSymbolConverter {
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 => {
format!("{}-{}-SWAP", 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>,
) -> Result<ParsedSymbol, String> {
let parts: Vec<&str> = exchange_id.split('-').collect();
if parts.len() < 2 {
return Err(format!("Invalid OKX exchange ID format: {}", exchange_id));
}
let base = parts[0].to_uppercase();
let quote = parts[1].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> {
let parts: Vec<&str> = exchange_id.split('-').collect();
if parts.len() >= 3 {
let date_part = parts[2];
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_swap(exchange_id: &str) -> bool {
exchange_id.ends_with("-SWAP")
}
pub fn is_futures(exchange_id: &str) -> bool {
let parts: Vec<&str> = exchange_id.split('-').collect();
if parts.len() >= 3 {
let suffix = parts[2];
suffix.len() == 6 && suffix.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
pub fn is_spot(exchange_id: &str) -> bool {
let parts: Vec<&str> = exchange_id.split('-').collect();
parts.len() == 2
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spot_to_exchange_id() {
let symbol = ParsedSymbol::spot("BTC".to_string(), "USDT".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(&symbol), "BTC-USDT");
}
#[test]
fn test_spot_to_exchange_id_various_pairs() {
let eth_usdt = ParsedSymbol::spot("ETH".to_string(), "USDT".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(ð_usdt), "ETH-USDT");
let btc_usdc = ParsedSymbol::spot("BTC".to_string(), "USDC".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(&btc_usdc), "BTC-USDC");
let sol_btc = ParsedSymbol::spot("SOL".to_string(), "BTC".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(&sol_btc), "SOL-BTC");
}
#[test]
fn test_linear_swap_to_exchange_id() {
let symbol = ParsedSymbol::linear_swap("BTC".to_string(), "USDT".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(&symbol), "BTC-USDT-SWAP");
}
#[test]
fn test_inverse_swap_to_exchange_id() {
let symbol = ParsedSymbol::inverse_swap("BTC".to_string(), "USD".to_string());
assert_eq!(OkxSymbolConverter::to_exchange_id(&symbol), "BTC-USD-SWAP");
}
#[test]
fn test_linear_futures_to_exchange_id() {
let expiry = ExpiryDate::new(24, 12, 31).unwrap();
let symbol = ParsedSymbol::futures(
"BTC".to_string(),
"USDT".to_string(),
"USDT".to_string(),
expiry,
);
assert_eq!(
OkxSymbolConverter::to_exchange_id(&symbol),
"BTC-USDT-241231"
);
}
#[test]
fn test_inverse_futures_to_exchange_id() {
let expiry = ExpiryDate::new(25, 3, 15).unwrap();
let symbol = ParsedSymbol::futures(
"BTC".to_string(),
"USD".to_string(),
"BTC".to_string(),
expiry,
);
assert_eq!(
OkxSymbolConverter::to_exchange_id(&symbol),
"BTC-USD-250315"
);
}
#[test]
fn test_futures_date_padding() {
let expiry = ExpiryDate::new(25, 1, 5).unwrap();
let symbol = ParsedSymbol::futures(
"ETH".to_string(),
"USDT".to_string(),
"USDT".to_string(),
expiry,
);
assert_eq!(
OkxSymbolConverter::to_exchange_id(&symbol),
"ETH-USDT-250105"
);
}
#[test]
fn test_spot_from_exchange_id() {
let parsed =
OkxSymbolConverter::from_exchange_id("BTC-USDT", SymbolMarketType::Spot, None).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 = OkxSymbolConverter::from_exchange_id(
"BTC-USDT-SWAP",
SymbolMarketType::Swap,
Some("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 = OkxSymbolConverter::from_exchange_id(
"BTC-USDT-241231",
SymbolMarketType::Futures,
Some("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_swap() {
assert!(OkxSymbolConverter::is_swap("BTC-USDT-SWAP"));
assert!(OkxSymbolConverter::is_swap("ETH-USD-SWAP"));
assert!(!OkxSymbolConverter::is_swap("BTC-USDT"));
assert!(!OkxSymbolConverter::is_swap("BTC-USDT-241231"));
}
#[test]
fn test_is_futures() {
assert!(OkxSymbolConverter::is_futures("BTC-USDT-241231"));
assert!(OkxSymbolConverter::is_futures("ETH-USDT-250315"));
assert!(!OkxSymbolConverter::is_futures("BTC-USDT"));
assert!(!OkxSymbolConverter::is_futures("BTC-USDT-SWAP"));
}
#[test]
fn test_is_spot() {
assert!(OkxSymbolConverter::is_spot("BTC-USDT"));
assert!(OkxSymbolConverter::is_spot("ETH-BTC"));
assert!(!OkxSymbolConverter::is_spot("BTC-USDT-SWAP"));
assert!(!OkxSymbolConverter::is_spot("BTC-USDT-241231"));
}
}