ccxt_core/traits/account.rs
1//! Account trait definition.
2//!
3//! The `Account` trait provides methods for account-related operations
4//! including fetching balances and trade history. These operations
5//! require authentication.
6//!
7//! # Timestamp Format
8//!
9//! All timestamp parameters and return values in this trait use the standardized format:
10//! - **Type**: `i64`
11//! - **Unit**: Milliseconds since Unix epoch (January 1, 1970, 00:00:00 UTC)
12//! - **Range**: Supports dates from 1970 to approximately year 294,276
13//!
14//! # Object Safety
15//!
16//! This trait is designed to be object-safe, allowing for dynamic dispatch via
17//! trait objects (`dyn Account`).
18//!
19//! # Example
20//!
21//! ```rust,ignore
22//! use ccxt_core::traits::Account;
23//! use ccxt_core::types::params::BalanceParams;
24//!
25//! async fn check_balance(exchange: &dyn Account) -> Result<(), ccxt_core::Error> {
26//! // Fetch spot balance
27//! let balance = exchange.fetch_balance().await?;
28//!
29//! // Fetch futures balance
30//! let balance = exchange.fetch_balance_with_params(BalanceParams::futures()).await?;
31//!
32//! // Get specific currency balance
33//! let btc = exchange.get_balance("BTC").await?;
34//!
35//! // Fetch trade history with i64 timestamp
36//! let since: i64 = chrono::Utc::now().timestamp_millis() - 86400000; // 24 hours ago
37//! let trades = exchange.fetch_my_trades_since("BTC/USDT", Some(since), Some(100)).await?;
38//!
39//! Ok(())
40//! }
41//! ```
42
43use async_trait::async_trait;
44
45use crate::error::{Error, Result};
46use crate::traits::PublicExchange;
47use crate::types::{Balance, BalanceEntry, Trade, params::BalanceParams};
48
49/// Trait for account-related operations.
50///
51/// This trait provides methods for fetching account balances and trade history.
52/// All methods require authentication and are async.
53///
54/// # Timestamp Format
55///
56/// All timestamp parameters and fields in returned data structures use:
57/// - **Type**: `i64`
58/// - **Unit**: Milliseconds since Unix epoch (January 1, 1970, 00:00:00 UTC)
59/// - **Example**: `1609459200000` represents January 1, 2021, 00:00:00 UTC
60///
61/// # Supertrait
62///
63/// Requires `PublicExchange` as a supertrait to access exchange metadata
64/// and capabilities.
65///
66/// # Thread Safety
67///
68/// This trait requires `Send + Sync` bounds (inherited from `PublicExchange`)
69/// to ensure safe usage across thread boundaries in async contexts.
70#[async_trait]
71pub trait Account: PublicExchange {
72 // ========================================================================
73 // Balance
74 // ========================================================================
75
76 /// Fetch account balance (default: spot account).
77 ///
78 /// Returns the current balance for all currencies in the account.
79 ///
80 /// # Example
81 ///
82 /// ```rust,ignore
83 /// let balance = exchange.fetch_balance().await?;
84 /// for (currency, entry) in &balance.currencies {
85 /// println!("{}: free={}, used={}", currency, entry.free, entry.used);
86 /// }
87 /// ```
88 async fn fetch_balance(&self) -> Result<Balance> {
89 self.fetch_balance_with_params(BalanceParams::default())
90 .await
91 }
92
93 /// Fetch balance with parameters.
94 ///
95 /// Allows specifying account type and currency filters.
96 ///
97 /// # Arguments
98 ///
99 /// * `params` - Balance parameters including account type and currency filters
100 ///
101 /// # Example
102 ///
103 /// ```rust,ignore
104 /// use ccxt_core::types::params::BalanceParams;
105 ///
106 /// // Futures balance
107 /// let balance = exchange.fetch_balance_with_params(BalanceParams::futures()).await?;
108 ///
109 /// // Specific currencies only
110 /// let balance = exchange.fetch_balance_with_params(
111 /// BalanceParams::spot().currencies(&["BTC", "USDT"])
112 /// ).await?;
113 /// ```
114 async fn fetch_balance_with_params(&self, params: BalanceParams) -> Result<Balance>;
115
116 /// Get balance for a specific currency.
117 ///
118 /// Convenience method that fetches the full balance and extracts
119 /// the entry for the specified currency.
120 ///
121 /// # Arguments
122 ///
123 /// * `currency` - Currency code (e.g., "BTC", "USDT")
124 ///
125 /// # Returns
126 ///
127 /// Returns the balance entry for the currency, or an error if not found.
128 ///
129 /// # Example
130 ///
131 /// ```rust,ignore
132 /// let btc = exchange.get_balance("BTC").await?;
133 /// println!("BTC: free={}, used={}, total={}", btc.free, btc.used, btc.total);
134 /// ```
135 async fn get_balance(&self, currency: &str) -> Result<BalanceEntry> {
136 let balance = self.fetch_balance().await?;
137 balance.get(currency).cloned().ok_or_else(|| {
138 Error::invalid_request(format!("Currency {currency} not found in balance"))
139 })
140 }
141
142 // ========================================================================
143 // Trade History
144 // ========================================================================
145
146 /// Fetch user's trade history for a symbol.
147 ///
148 /// Returns the user's executed trades for the specified symbol.
149 ///
150 /// # Arguments
151 ///
152 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
153 ///
154 /// # Example
155 ///
156 /// ```rust,ignore
157 /// let trades = exchange.fetch_my_trades("BTC/USDT").await?;
158 /// for trade in trades {
159 /// println!("{}: {} {} @ {}", trade.id, trade.side, trade.amount, trade.price);
160 /// }
161 /// ```
162 async fn fetch_my_trades(&self, symbol: &str) -> Result<Vec<Trade>> {
163 self.fetch_my_trades_since(symbol, None, None).await
164 }
165
166 /// Fetch trades with pagination.
167 ///
168 /// # Arguments
169 ///
170 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
171 /// * `since` - Optional start timestamp in milliseconds (i64) since Unix epoch
172 /// * `limit` - Optional maximum number of trades to return
173 ///
174 /// # Timestamp Format
175 ///
176 /// The `since` parameter uses `i64` milliseconds since Unix epoch:
177 /// - `1609459200000` = January 1, 2021, 00:00:00 UTC
178 /// - `chrono::Utc::now().timestamp_millis()` = Current time
179 /// - `chrono::Utc::now().timestamp_millis() - 86400000` = 24 hours ago
180 ///
181 /// # Example
182 ///
183 /// ```rust,ignore
184 /// // Recent trades (no timestamp filter)
185 /// let trades = exchange.fetch_my_trades_since("BTC/USDT", None, Some(100)).await?;
186 ///
187 /// // Trades from the last 24 hours
188 /// let since = chrono::Utc::now().timestamp_millis() - 86400000;
189 /// let trades = exchange.fetch_my_trades_since(
190 /// "BTC/USDT",
191 /// Some(since),
192 /// Some(50)
193 /// ).await?;
194 /// ```
195 async fn fetch_my_trades_since(
196 &self,
197 symbol: &str,
198 since: Option<i64>,
199 limit: Option<u32>,
200 ) -> Result<Vec<Trade>>;
201
202 // ========================================================================
203 // Deprecated u64 Wrapper Methods (Backward Compatibility)
204 // ========================================================================
205
206 /// Fetch user's trade history with u64 timestamp filtering (deprecated).
207 ///
208 /// **DEPRECATED**: Use `fetch_my_trades_since` with i64 timestamps instead.
209 /// This method is provided for backward compatibility during migration.
210 ///
211 /// # Migration
212 ///
213 /// ```rust,ignore
214 /// // Old code (deprecated)
215 /// let trades = exchange.fetch_my_trades_since_u64("BTC/USDT", Some(1609459200000u64), Some(100)).await?;
216 ///
217 /// // New code (recommended)
218 /// let trades = exchange.fetch_my_trades_since("BTC/USDT", Some(1609459200000i64), Some(100)).await?;
219 /// ```
220 #[deprecated(
221 since = "0.1.0",
222 note = "Use fetch_my_trades_since with i64 timestamps. Convert using TimestampUtils::u64_to_i64()"
223 )]
224 async fn fetch_my_trades_since_u64(
225 &self,
226 symbol: &str,
227 since: Option<u64>,
228 limit: Option<u32>,
229 ) -> Result<Vec<Trade>> {
230 use crate::time::TimestampConversion;
231
232 let since_i64 = since.to_i64()?;
233 self.fetch_my_trades_since(symbol, since_i64, limit).await
234 }
235}
236
237/// Type alias for boxed Account trait object.
238pub type BoxedAccount = Box<dyn Account>;
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::capability::ExchangeCapabilities;
244 use crate::types::Timeframe;
245
246 // Mock implementation for testing trait object safety
247 struct MockExchange;
248
249 impl PublicExchange for MockExchange {
250 fn id(&self) -> &str {
251 "mock"
252 }
253 fn name(&self) -> &str {
254 "Mock Exchange"
255 }
256 fn capabilities(&self) -> ExchangeCapabilities {
257 ExchangeCapabilities::all()
258 }
259 fn timeframes(&self) -> Vec<Timeframe> {
260 vec![Timeframe::H1]
261 }
262 }
263
264 #[async_trait]
265 impl Account for MockExchange {
266 async fn fetch_balance_with_params(&self, _params: BalanceParams) -> Result<Balance> {
267 let mut balance = Balance::new();
268 balance.set(
269 "BTC".to_string(),
270 BalanceEntry {
271 free: rust_decimal_macros::dec!(1.5),
272 used: rust_decimal_macros::dec!(0.5),
273 total: rust_decimal_macros::dec!(2.0),
274 },
275 );
276 balance.set(
277 "USDT".to_string(),
278 BalanceEntry {
279 free: rust_decimal_macros::dec!(10000),
280 used: rust_decimal_macros::dec!(5000),
281 total: rust_decimal_macros::dec!(15000),
282 },
283 );
284
285 Ok(balance)
286 }
287
288 async fn fetch_my_trades_since(
289 &self,
290 _symbol: &str,
291 _since: Option<i64>,
292 _limit: Option<u32>,
293 ) -> Result<Vec<Trade>> {
294 Ok(vec![])
295 }
296 }
297
298 #[test]
299 fn test_trait_object_safety() {
300 // Verify trait is object-safe by creating a trait object
301 let _exchange: BoxedAccount = Box::new(MockExchange);
302 }
303
304 #[tokio::test]
305 async fn test_fetch_balance() {
306 let exchange = MockExchange;
307
308 let balance = exchange.fetch_balance().await.unwrap();
309 assert!(balance.get("BTC").is_some());
310 assert!(balance.get("USDT").is_some());
311 }
312
313 #[tokio::test]
314 async fn test_fetch_balance_with_params() {
315 let exchange = MockExchange;
316
317 let balance = exchange
318 .fetch_balance_with_params(BalanceParams::futures())
319 .await
320 .unwrap();
321 assert!(balance.get("BTC").is_some());
322 }
323
324 #[tokio::test]
325 async fn test_get_balance() {
326 let exchange = MockExchange;
327
328 let btc = exchange.get_balance("BTC").await.unwrap();
329 assert_eq!(btc.free, rust_decimal_macros::dec!(1.5));
330 assert_eq!(btc.total, rust_decimal_macros::dec!(2.0));
331
332 // Test non-existent currency
333 let result = exchange.get_balance("XYZ").await;
334 assert!(result.is_err());
335 }
336
337 #[tokio::test]
338 async fn test_fetch_my_trades() {
339 let exchange = MockExchange;
340
341 let trades = exchange.fetch_my_trades("BTC/USDT").await.unwrap();
342 assert!(trades.is_empty());
343 }
344
345 #[tokio::test]
346 async fn test_fetch_my_trades_since() {
347 let exchange = MockExchange;
348
349 let trades = exchange
350 .fetch_my_trades_since("BTC/USDT", Some(1609459200000), Some(100))
351 .await
352 .unwrap();
353 assert!(trades.is_empty());
354 }
355}