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}