ccxt_core/symbol/formatter.rs
1//! Symbol formatter implementation
2//!
3//! This module provides formatting functionality for converting `ParsedSymbol`
4//! structures back into unified symbol strings.
5//!
6//! # Symbol Format
7//!
8//! The unified symbol format follows the CCXT standard:
9//! - **Spot**: `BASE/QUOTE` (e.g., "BTC/USDT")
10//! - **Perpetual Swap**: `BASE/QUOTE:SETTLE` (e.g., "BTC/USDT:USDT")
11//! - **Futures**: `BASE/QUOTE:SETTLE-YYMMDD` (e.g., "BTC/USDT:USDT-241231")
12//!
13//! # Example
14//!
15//! ```rust
16//! use ccxt_core::symbol::{ParsedSymbol, SymbolFormatter};
17//! use ccxt_core::types::symbol::ExpiryDate;
18//!
19//! // Format a spot symbol
20//! let spot = ParsedSymbol::spot("BTC".to_string(), "USDT".to_string());
21//! assert_eq!(SymbolFormatter::format(&spot), "BTC/USDT");
22//!
23//! // Format a swap symbol
24//! let swap = ParsedSymbol::linear_swap("BTC".to_string(), "USDT".to_string());
25//! assert_eq!(SymbolFormatter::format(&swap), "BTC/USDT:USDT");
26//!
27//! // Format a futures symbol
28//! let expiry = ExpiryDate::new(24, 12, 31).unwrap();
29//! let futures = ParsedSymbol::futures("BTC".to_string(), "USDT".to_string(), "USDT".to_string(), expiry);
30//! assert_eq!(SymbolFormatter::format(&futures), "BTC/USDT:USDT-241231");
31//! ```
32
33use crate::types::symbol::ParsedSymbol;
34
35/// Symbol formatter for converting ParsedSymbol to unified symbol strings
36///
37/// Provides methods to format `ParsedSymbol` structures into their
38/// canonical unified symbol string representation.
39pub struct SymbolFormatter;
40
41impl SymbolFormatter {
42 /// Format a `ParsedSymbol` into a unified symbol string
43 ///
44 /// # Arguments
45 ///
46 /// * `parsed` - The parsed symbol to format
47 ///
48 /// # Returns
49 ///
50 /// Returns the unified symbol string in the appropriate format:
51 /// - Spot: `BASE/QUOTE`
52 /// - Swap: `BASE/QUOTE:SETTLE`
53 /// - Futures: `BASE/QUOTE:SETTLE-YYMMDD`
54 ///
55 /// # Examples
56 ///
57 /// ```rust
58 /// use ccxt_core::symbol::{ParsedSymbol, SymbolFormatter};
59 /// use ccxt_core::types::symbol::ExpiryDate;
60 ///
61 /// // Spot symbol
62 /// let spot = ParsedSymbol::spot("BTC".to_string(), "USDT".to_string());
63 /// assert_eq!(SymbolFormatter::format(&spot), "BTC/USDT");
64 ///
65 /// // Linear swap symbol
66 /// let swap = ParsedSymbol::linear_swap("ETH".to_string(), "USDT".to_string());
67 /// assert_eq!(SymbolFormatter::format(&swap), "ETH/USDT:USDT");
68 ///
69 /// // Inverse swap symbol
70 /// let inverse = ParsedSymbol::inverse_swap("BTC".to_string(), "USD".to_string());
71 /// assert_eq!(SymbolFormatter::format(&inverse), "BTC/USD:BTC");
72 ///
73 /// // Futures symbol
74 /// let expiry = ExpiryDate::new(24, 12, 31).unwrap();
75 /// let futures = ParsedSymbol::futures("BTC".to_string(), "USDT".to_string(), "USDT".to_string(), expiry);
76 /// assert_eq!(SymbolFormatter::format(&futures), "BTC/USDT:USDT-241231");
77 /// ```
78 pub fn format(parsed: &ParsedSymbol) -> String {
79 // Use the Display implementation which already handles all cases
80 parsed.to_string()
81 }
82
83 /// Format a spot symbol from base and quote currencies
84 ///
85 /// # Arguments
86 ///
87 /// * `base` - Base currency code
88 /// * `quote` - Quote currency code
89 ///
90 /// # Returns
91 ///
92 /// Returns the formatted spot symbol string `BASE/QUOTE`
93 pub fn format_spot(base: &str, quote: &str) -> String {
94 format!("{}/{}", base.to_uppercase(), quote.to_uppercase())
95 }
96
97 /// Format a swap symbol from base, quote, and settle currencies
98 ///
99 /// # Arguments
100 ///
101 /// * `base` - Base currency code
102 /// * `quote` - Quote currency code
103 /// * `settle` - Settlement currency code
104 ///
105 /// # Returns
106 ///
107 /// Returns the formatted swap symbol string `BASE/QUOTE:SETTLE`
108 pub fn format_swap(base: &str, quote: &str, settle: &str) -> String {
109 format!(
110 "{}/{}:{}",
111 base.to_uppercase(),
112 quote.to_uppercase(),
113 settle.to_uppercase()
114 )
115 }
116
117 /// Format a futures symbol from base, quote, settle currencies and expiry date
118 ///
119 /// # Arguments
120 ///
121 /// * `base` - Base currency code
122 /// * `quote` - Quote currency code
123 /// * `settle` - Settlement currency code
124 /// * `year` - 2-digit year (0-99)
125 /// * `month` - Month (1-12)
126 /// * `day` - Day (1-31)
127 ///
128 /// # Returns
129 ///
130 /// Returns the formatted futures symbol string `BASE/QUOTE:SETTLE-YYMMDD`
131 pub fn format_futures(
132 base: &str,
133 quote: &str,
134 settle: &str,
135 year: u8,
136 month: u8,
137 day: u8,
138 ) -> String {
139 format!(
140 "{}/{}:{}-{:02}{:02}{:02}",
141 base.to_uppercase(),
142 quote.to_uppercase(),
143 settle.to_uppercase(),
144 year,
145 month,
146 day
147 )
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::types::symbol::ExpiryDate;
155
156 #[test]
157 fn test_format_spot() {
158 let symbol = ParsedSymbol::spot("BTC".to_string(), "USDT".to_string());
159 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USDT");
160 }
161
162 #[test]
163 fn test_format_spot_lowercase_input() {
164 let symbol = ParsedSymbol::spot("btc".to_string(), "usdt".to_string());
165 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USDT");
166 }
167
168 #[test]
169 fn test_format_linear_swap() {
170 let symbol = ParsedSymbol::linear_swap("BTC".to_string(), "USDT".to_string());
171 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USDT:USDT");
172 }
173
174 #[test]
175 fn test_format_inverse_swap() {
176 let symbol = ParsedSymbol::inverse_swap("BTC".to_string(), "USD".to_string());
177 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USD:BTC");
178 }
179
180 #[test]
181 fn test_format_futures() {
182 let expiry = ExpiryDate::new(24, 12, 31).unwrap();
183 let symbol = ParsedSymbol::futures(
184 "BTC".to_string(),
185 "USDT".to_string(),
186 "USDT".to_string(),
187 expiry,
188 );
189 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USDT:USDT-241231");
190 }
191
192 #[test]
193 fn test_format_futures_inverse() {
194 let expiry = ExpiryDate::new(25, 3, 15).unwrap();
195 let symbol = ParsedSymbol::futures(
196 "BTC".to_string(),
197 "USD".to_string(),
198 "BTC".to_string(),
199 expiry,
200 );
201 assert_eq!(SymbolFormatter::format(&symbol), "BTC/USD:BTC-250315");
202 }
203
204 #[test]
205 fn test_format_spot_helper() {
206 assert_eq!(SymbolFormatter::format_spot("BTC", "USDT"), "BTC/USDT");
207 assert_eq!(SymbolFormatter::format_spot("btc", "usdt"), "BTC/USDT");
208 }
209
210 #[test]
211 fn test_format_swap_helper() {
212 assert_eq!(
213 SymbolFormatter::format_swap("BTC", "USDT", "USDT"),
214 "BTC/USDT:USDT"
215 );
216 assert_eq!(
217 SymbolFormatter::format_swap("btc", "usd", "btc"),
218 "BTC/USD:BTC"
219 );
220 }
221
222 #[test]
223 fn test_format_futures_helper() {
224 assert_eq!(
225 SymbolFormatter::format_futures("BTC", "USDT", "USDT", 24, 12, 31),
226 "BTC/USDT:USDT-241231"
227 );
228 assert_eq!(
229 SymbolFormatter::format_futures("btc", "usd", "btc", 25, 1, 5),
230 "BTC/USD:BTC-250105"
231 );
232 }
233
234 #[test]
235 fn test_format_futures_date_padding() {
236 // Ensure single-digit months and days are zero-padded
237 assert_eq!(
238 SymbolFormatter::format_futures("ETH", "USDT", "USDT", 25, 1, 5),
239 "ETH/USDT:USDT-250105"
240 );
241 }
242}