rusty_common/
types.rs

1//! Common types used across exchanges
2
3use crate::SmartString;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Supported exchanges
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Exchange {
11    /// Binance
12    Binance,
13    /// Coinbase
14    Coinbase,
15    /// Bybit
16    Bybit,
17    /// Upbit
18    Upbit,
19    /// Bithumb
20    Bithumb,
21}
22
23impl Exchange {
24    /// Get exchange name as static string at compile time
25    #[inline]
26    #[must_use]
27    pub const fn as_static_str(self) -> &'static str {
28        match self {
29            Self::Binance => "Binance",
30            Self::Coinbase => "Coinbase",
31            Self::Bybit => "Bybit",
32            Self::Upbit => "Upbit",
33            Self::Bithumb => "Bithumb",
34        }
35    }
36
37    /// Get exchange count for array sizing
38    #[inline]
39    #[must_use]
40    pub const fn count() -> usize {
41        5
42    }
43
44    /// Check if exchange supports spot trading (compile-time known)
45    #[inline]
46    #[must_use]
47    pub const fn supports_spot(self) -> bool {
48        match self {
49            Self::Binance | Self::Coinbase | Self::Bybit | Self::Upbit | Self::Bithumb => true,
50        }
51    }
52
53    /// Check if exchange supports futures trading (compile-time known)
54    #[inline]
55    #[must_use]
56    pub const fn supports_futures(self) -> bool {
57        match self {
58            Self::Binance | Self::Bybit => true,
59            Self::Coinbase | Self::Upbit | Self::Bithumb => false,
60        }
61    }
62}
63
64impl fmt::Display for Exchange {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "{}", self.as_static_str())
67    }
68}
69
70/// Price type alias for decimal prices
71pub type Price = Decimal;
72
73/// Quantity type alias for decimal quantities
74pub type Quantity = Decimal;
75
76/// Symbol type representing a trading pair (e.g., "BTC-USDT", "ETH-BTC")
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Symbol(SmartString);
79
80impl Symbol {
81    /// Create a new symbol
82    #[must_use]
83    pub fn new(s: impl AsRef<str>) -> Self {
84        Self(s.as_ref().into())
85    }
86
87    /// Get the symbol as a string
88    pub fn as_str(&self) -> &str {
89        self.0.as_str()
90    }
91
92    /// Parse symbol into base and quote currencies
93    /// Assumes format like "BTC-USDT" or "BTC/USDT"
94    #[must_use]
95    pub fn parse(&self) -> Option<(&str, &str)> {
96        // Try different separators
97        if let Some(pos) = self.0.find('-') {
98            Some((&self.0[..pos], &self.0[pos + 1..]))
99        } else if let Some(pos) = self.0.find('/') {
100            Some((&self.0[..pos], &self.0[pos + 1..]))
101        } else if let Some(pos) = self.0.find('_') {
102            Some((&self.0[..pos], &self.0[pos + 1..]))
103        } else {
104            None
105        }
106    }
107}
108
109impl fmt::Display for Symbol {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115impl From<SmartString> for Symbol {
116    fn from(s: SmartString) -> Self {
117        Self(s)
118    }
119}
120
121impl From<&str> for Symbol {
122    fn from(s: &str) -> Self {
123        Self(s.into())
124    }
125}
126
127/// Compile-time validation and utility functions
128/// Check if a character is a valid symbol separator
129#[inline]
130#[must_use]
131pub const fn is_valid_symbol_separator(c: char) -> bool {
132    matches!(c, '-' | '/' | '_')
133}
134
135/// Check if a character is alphanumeric (for symbol validation)
136#[inline]
137#[must_use]
138pub const fn is_alphanumeric_ascii(c: char) -> bool {
139    matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9')
140}
141
142/// Calculate maximum symbol length for buffer sizing
143#[inline]
144#[must_use]
145pub const fn max_symbol_length() -> usize {
146    32 // Common maximum for most exchanges
147}
148
149/// Calculate maximum API key length for buffer sizing
150#[inline]
151#[must_use]
152pub const fn max_api_key_length() -> usize {
153    256 // Conservative maximum for API keys
154}
155
156/// Check if exchange requires passphrase at compile time
157#[inline]
158#[must_use]
159pub const fn exchange_requires_passphrase(exchange: Exchange) -> bool {
160    match exchange {
161        Exchange::Coinbase => true,
162        Exchange::Binance | Exchange::Bybit | Exchange::Upbit | Exchange::Bithumb => false,
163    }
164}
165
166/// Get default WebSocket port for exchange
167#[inline]
168#[must_use]
169pub const fn exchange_default_ws_port(exchange: Exchange) -> u16 {
170    match exchange {
171        Exchange::Binance
172        | Exchange::Coinbase
173        | Exchange::Bybit
174        | Exchange::Upbit
175        | Exchange::Bithumb => 443,
176    }
177}
178
179#[cfg(test)]
180mod const_fn_tests {
181    use super::*;
182
183    #[test]
184    fn test_exchange_const_functions() {
185        // Test const fn operations can be used in const contexts
186        const BINANCE_NAME: &str = Exchange::Binance.as_static_str();
187        const EXCHANGE_COUNT: usize = Exchange::count();
188        const SPOT_SUPPORT: bool = Exchange::Binance.supports_spot();
189        const FUTURES_SUPPORT: bool = Exchange::Coinbase.supports_futures();
190
191        assert_eq!(BINANCE_NAME, "Binance");
192        assert_eq!(EXCHANGE_COUNT, 5);
193        assert!(SPOT_SUPPORT);
194        assert!(!FUTURES_SUPPORT);
195    }
196
197    #[test]
198    fn test_validation_const_functions() {
199        // Test validation functions in const contexts
200        const IS_DASH_SEPARATOR: bool = is_valid_symbol_separator('-');
201        const IS_ALPHANUMERIC: bool = is_alphanumeric_ascii('A');
202        const MAX_SYMBOL_LEN: usize = max_symbol_length();
203        const COINBASE_NEEDS_PASSPHRASE: bool = exchange_requires_passphrase(Exchange::Coinbase);
204        const WS_PORT: u16 = exchange_default_ws_port(Exchange::Binance);
205
206        assert!(IS_DASH_SEPARATOR);
207        assert!(IS_ALPHANUMERIC);
208        assert_eq!(MAX_SYMBOL_LEN, 32);
209        assert!(COINBASE_NEEDS_PASSPHRASE);
210        assert_eq!(WS_PORT, 443);
211    }
212}
213
214/// Common API response wrapper
215#[derive(Debug, Serialize, Deserialize)]
216pub struct ApiResponse<T> {
217    /// Whether the request was successful.
218    pub success: bool,
219    /// The data returned by the API.
220    pub data: Option<T>,
221    /// The error returned by the API, if any.
222    pub error: Option<ApiError>,
223}
224
225/// Common API error structure
226#[derive(Debug, Serialize, Deserialize)]
227pub struct ApiError {
228    /// The error code.
229    pub code: SmartString,
230    /// The error message.
231    pub message: SmartString,
232    /// Additional details about the error.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub details: Option<crate::json::Value>,
235}
236
237/// Rate limit information
238#[derive(Debug, Clone)]
239pub struct RateLimitInfo {
240    /// The rate limit.
241    pub limit: u32,
242    /// The remaining requests in the current window.
243    pub remaining: u32,
244    /// The time when the rate limit resets, as a Unix timestamp.
245    pub reset_at: u64, // Unix timestamp
246}
247
248/// WebSocket subscription request
249#[derive(Debug, Serialize, Deserialize)]
250pub struct WsSubscription {
251    /// The channel to subscribe to.
252    pub channel: SmartString,
253    /// The symbols to subscribe to.
254    pub symbols: Vec<Symbol>,
255    /// Additional parameters for the subscription.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub params: Option<crate::json::Value>,
258}