1use crate::SmartString;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Exchange {
11 Binance,
13 Coinbase,
15 Bybit,
17 Upbit,
19 Bithumb,
21}
22
23impl Exchange {
24 #[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 #[inline]
39 #[must_use]
40 pub const fn count() -> usize {
41 5
42 }
43
44 #[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 #[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
70pub type Price = Decimal;
72
73pub type Quantity = Decimal;
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Symbol(SmartString);
79
80impl Symbol {
81 #[must_use]
83 pub fn new(s: impl AsRef<str>) -> Self {
84 Self(s.as_ref().into())
85 }
86
87 pub fn as_str(&self) -> &str {
89 self.0.as_str()
90 }
91
92 #[must_use]
95 pub fn parse(&self) -> Option<(&str, &str)> {
96 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#[inline]
130#[must_use]
131pub const fn is_valid_symbol_separator(c: char) -> bool {
132 matches!(c, '-' | '/' | '_')
133}
134
135#[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#[inline]
144#[must_use]
145pub const fn max_symbol_length() -> usize {
146 32 }
148
149#[inline]
151#[must_use]
152pub const fn max_api_key_length() -> usize {
153 256 }
155
156#[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#[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 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 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#[derive(Debug, Serialize, Deserialize)]
216pub struct ApiResponse<T> {
217 pub success: bool,
219 pub data: Option<T>,
221 pub error: Option<ApiError>,
223}
224
225#[derive(Debug, Serialize, Deserialize)]
227pub struct ApiError {
228 pub code: SmartString,
230 pub message: SmartString,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub details: Option<crate::json::Value>,
235}
236
237#[derive(Debug, Clone)]
239pub struct RateLimitInfo {
240 pub limit: u32,
242 pub remaining: u32,
244 pub reset_at: u64, }
247
248#[derive(Debug, Serialize, Deserialize)]
250pub struct WsSubscription {
251 pub channel: SmartString,
253 pub symbols: Vec<Symbol>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub params: Option<crate::json::Value>,
258}