nautilus_binance/common/
symbol.rs1use nautilus_model::identifiers::InstrumentId;
19use ustr::Ustr;
20
21use super::{consts::BINANCE_VENUE, enums::BinanceProductType};
22
23#[must_use]
35pub fn format_instrument_id(symbol: &Ustr, product_type: BinanceProductType) -> InstrumentId {
36 let nautilus_symbol = match product_type {
37 BinanceProductType::UsdM => {
38 if is_delivery_symbol(symbol.as_str()) {
39 symbol.to_string()
40 } else {
41 format!("{symbol}-PERP")
42 }
43 }
44 BinanceProductType::CoinM => {
45 symbol.to_string()
47 }
48 _ => symbol.to_string(),
49 };
50 InstrumentId::new(nautilus_symbol.into(), *BINANCE_VENUE)
51}
52
53#[must_use]
64pub fn format_binance_symbol(instrument_id: &InstrumentId) -> String {
65 let symbol = instrument_id.symbol.as_str();
66
67 if symbol.ends_with("-PERP") {
68 symbol.trim_end_matches("-PERP").to_string()
69 } else {
70 symbol.to_string()
71 }
72}
73
74#[must_use]
78pub fn format_binance_stream_symbol(instrument_id: &InstrumentId) -> String {
79 format_binance_symbol(instrument_id).to_lowercase()
80}
81
82fn is_delivery_symbol(symbol: &str) -> bool {
83 symbol
84 .rsplit_once('_')
85 .is_some_and(|(_, expiry)| expiry.len() == 6 && expiry.bytes().all(|b| b.is_ascii_digit()))
86}
87
88#[cfg(test)]
89mod tests {
90 use rstest::rstest;
91
92 use super::*;
93
94 #[rstest]
95 #[case("BTCUSDT-PERP.BINANCE", "BTCUSDT")]
96 #[case("ETHUSDT-PERP.BINANCE", "ETHUSDT")]
97 #[case("BTCUSD_PERP.BINANCE", "BTCUSD_PERP")]
98 #[case("BTCUSDT.BINANCE", "BTCUSDT")]
99 #[case("ETHBTC.BINANCE", "ETHBTC")]
100 fn test_format_binance_symbol(#[case] input: &str, #[case] expected: &str) {
101 let instrument_id = InstrumentId::from(input);
102 assert_eq!(format_binance_symbol(&instrument_id), expected);
103 }
104
105 #[rstest]
106 #[case("BTCUSDT-PERP.BINANCE", "btcusdt")]
107 #[case("ETHUSDT-PERP.BINANCE", "ethusdt")]
108 #[case("BTCUSD_PERP.BINANCE", "btcusd_perp")]
109 fn test_format_binance_stream_symbol(#[case] input: &str, #[case] expected: &str) {
110 let instrument_id = InstrumentId::from(input);
111 assert_eq!(format_binance_stream_symbol(&instrument_id), expected);
112 }
113
114 #[rstest]
115 #[case::usdm_perp("BTCUSDT", BinanceProductType::UsdM, "BTCUSDT-PERP.BINANCE")]
116 #[case::usdm_eth("ETHUSDT", BinanceProductType::UsdM, "ETHUSDT-PERP.BINANCE")]
117 #[case::usdm_delivery("BTCUSDT_260925", BinanceProductType::UsdM, "BTCUSDT_260925.BINANCE")]
118 #[case::usdm_digit_quote("SPCXUSD1", BinanceProductType::UsdM, "SPCXUSD1-PERP.BINANCE")]
119 #[case::coinm_perp("BTCUSD_PERP", BinanceProductType::CoinM, "BTCUSD_PERP.BINANCE")]
120 #[case::coinm_eth("ETHUSD_PERP", BinanceProductType::CoinM, "ETHUSD_PERP.BINANCE")]
121 #[case::spot("BTCUSDT", BinanceProductType::Spot, "BTCUSDT.BINANCE")]
122 #[case::spot_eth("ETHBTC", BinanceProductType::Spot, "ETHBTC.BINANCE")]
123 #[case::margin("BTCUSDT", BinanceProductType::Margin, "BTCUSDT.BINANCE")]
124 #[case::options(
125 "BTC-240329-70000-C",
126 BinanceProductType::Options,
127 "BTC-240329-70000-C.BINANCE"
128 )]
129 fn test_format_instrument_id(
130 #[case] raw_symbol: &str,
131 #[case] product_type: BinanceProductType,
132 #[case] expected: &str,
133 ) {
134 let symbol = Ustr::from(raw_symbol);
135 let instrument_id = format_instrument_id(&symbol, product_type);
136 assert_eq!(instrument_id.to_string(), expected);
137 }
138}