Skip to main content

ccxt_core/traits/
margin.rs

1//! Margin trait definition.
2//!
3//! The `Margin` trait provides methods for margin and futures trading operations
4//! including position management, leverage configuration, and funding rate queries.
5//! These operations 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 Margin`).
18//!
19//! # Example
20//!
21//! ```rust,ignore
22//! use ccxt_core::traits::Margin;
23//! use ccxt_core::types::params::LeverageParams;
24//!
25//! async fn manage_positions(exchange: &dyn Margin) -> Result<(), ccxt_core::Error> {
26//!     // Fetch all positions
27//!     let positions = exchange.fetch_positions().await?;
28//!     
29//!     // Set leverage
30//!     exchange.set_leverage("BTC/USDT:USDT", 10).await?;
31//!     
32//!     // Fetch funding rate history with i64 timestamp
33//!     let since: i64 = chrono::Utc::now().timestamp_millis() - 604800000; // 7 days ago
34//!     let history = exchange.fetch_funding_rate_history("BTC/USDT:USDT", Some(since), Some(100)).await?;
35//!     
36//!     Ok(())
37//! }
38//! ```
39
40use async_trait::async_trait;
41
42use crate::error::Result;
43use crate::traits::PublicExchange;
44use crate::types::{
45    FundingRate, FundingRateHistory, Position,
46    params::{LeverageParams, MarginMode},
47};
48
49/// Trait for margin and futures trading operations.
50///
51/// This trait provides methods for managing positions, leverage, margin mode,
52/// and funding rates. 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 Margin: PublicExchange {
72    // ========================================================================
73    // Position Management
74    // ========================================================================
75
76    /// Fetch all positions.
77    ///
78    /// Returns all open positions across all symbols.
79    ///
80    /// # Example
81    ///
82    /// ```rust,ignore
83    /// let positions = exchange.fetch_positions().await?;
84    /// for pos in positions {
85    ///     println!("{}: {} contracts @ {}", pos.symbol, pos.contracts.unwrap_or(0.0), pos.entry_price.unwrap_or(0.0));
86    /// }
87    /// ```
88    async fn fetch_positions(&self) -> Result<Vec<Position>> {
89        self.fetch_positions_for(&[]).await
90    }
91
92    /// Fetch positions for specific symbols.
93    ///
94    /// # Arguments
95    ///
96    /// * `symbols` - Array of trading pair symbols (e.g., ["BTC/USDT:USDT", "ETH/USDT:USDT"])
97    ///
98    /// # Example
99    ///
100    /// ```rust,ignore
101    /// let positions = exchange.fetch_positions_for(&["BTC/USDT:USDT", "ETH/USDT:USDT"]).await?;
102    /// ```
103    async fn fetch_positions_for(&self, symbols: &[&str]) -> Result<Vec<Position>>;
104
105    /// Fetch a single position for a symbol.
106    ///
107    /// # Arguments
108    ///
109    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT")
110    ///
111    /// # Example
112    ///
113    /// ```rust,ignore
114    /// let position = exchange.fetch_position("BTC/USDT:USDT").await?;
115    /// println!("PnL: {}", position.unrealized_pnl.unwrap_or(0.0));
116    /// ```
117    async fn fetch_position(&self, symbol: &str) -> Result<Position>;
118
119    // ========================================================================
120    // Leverage Management
121    // ========================================================================
122
123    /// Set leverage for a symbol.
124    ///
125    /// # Arguments
126    ///
127    /// * `symbol` - Trading pair symbol
128    /// * `leverage` - Leverage multiplier (e.g., 10 for 10x)
129    ///
130    /// # Example
131    ///
132    /// ```rust,ignore
133    /// exchange.set_leverage("BTC/USDT:USDT", 10).await?;
134    /// ```
135    async fn set_leverage(&self, symbol: &str, leverage: u32) -> Result<()> {
136        self.set_leverage_with_params(LeverageParams::new(symbol, leverage))
137            .await
138    }
139
140    /// Set leverage with full parameters.
141    ///
142    /// Allows specifying margin mode along with leverage.
143    ///
144    /// # Arguments
145    ///
146    /// * `params` - Leverage parameters including symbol, leverage, and optional margin mode
147    ///
148    /// # Example
149    ///
150    /// ```rust,ignore
151    /// use ccxt_core::types::params::LeverageParams;
152    ///
153    /// exchange.set_leverage_with_params(
154    ///     LeverageParams::new("BTC/USDT:USDT", 10).isolated()
155    /// ).await?;
156    /// ```
157    async fn set_leverage_with_params(&self, params: LeverageParams) -> Result<()>;
158
159    /// Get current leverage for a symbol.
160    ///
161    /// # Arguments
162    ///
163    /// * `symbol` - Trading pair symbol
164    ///
165    /// # Returns
166    ///
167    /// Returns the current leverage multiplier.
168    ///
169    /// # Example
170    ///
171    /// ```rust,ignore
172    /// let leverage = exchange.get_leverage("BTC/USDT:USDT").await?;
173    /// println!("Current leverage: {}x", leverage);
174    /// ```
175    async fn get_leverage(&self, symbol: &str) -> Result<u32>;
176
177    // ========================================================================
178    // Margin Mode
179    // ========================================================================
180
181    /// Set margin mode for a symbol.
182    ///
183    /// # Arguments
184    ///
185    /// * `symbol` - Trading pair symbol
186    /// * `mode` - Margin mode (Cross or Isolated)
187    ///
188    /// # Example
189    ///
190    /// ```rust,ignore
191    /// use ccxt_core::types::params::MarginMode;
192    ///
193    /// exchange.set_margin_mode("BTC/USDT:USDT", MarginMode::Isolated).await?;
194    /// ```
195    async fn set_margin_mode(&self, symbol: &str, mode: MarginMode) -> Result<()>;
196
197    // ========================================================================
198    // Funding Rates
199    // ========================================================================
200
201    /// Fetch current funding rate for a symbol.
202    ///
203    /// # Arguments
204    ///
205    /// * `symbol` - Trading pair symbol
206    ///
207    /// # Example
208    ///
209    /// ```rust,ignore
210    /// let rate = exchange.fetch_funding_rate("BTC/USDT:USDT").await?;
211    /// println!("Funding rate: {}", rate.funding_rate.unwrap_or(0.0));
212    /// ```
213    async fn fetch_funding_rate(&self, symbol: &str) -> Result<FundingRate>;
214
215    /// Fetch funding rates for multiple symbols.
216    ///
217    /// # Arguments
218    ///
219    /// * `symbols` - Array of trading pair symbols
220    ///
221    /// # Example
222    ///
223    /// ```rust,ignore
224    /// let rates = exchange.fetch_funding_rates(&["BTC/USDT:USDT", "ETH/USDT:USDT"]).await?;
225    /// ```
226    async fn fetch_funding_rates(&self, symbols: &[&str]) -> Result<Vec<FundingRate>>;
227
228    /// Fetch funding rate history for a symbol.
229    ///
230    /// # Arguments
231    ///
232    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT" for futures)
233    /// * `since` - Optional start timestamp in milliseconds (i64) since Unix epoch
234    /// * `limit` - Optional maximum number of records to return
235    ///
236    /// # Timestamp Format
237    ///
238    /// The `since` parameter uses `i64` milliseconds since Unix epoch:
239    /// - `1609459200000` = January 1, 2021, 00:00:00 UTC
240    /// - `chrono::Utc::now().timestamp_millis()` = Current time
241    /// - `chrono::Utc::now().timestamp_millis() - 604800000` = 7 days ago
242    ///
243    /// # Example
244    ///
245    /// ```rust,ignore
246    /// // Fetch recent funding rate history
247    /// let history = exchange.fetch_funding_rate_history(
248    ///     "BTC/USDT:USDT",
249    ///     None,
250    ///     Some(100)
251    /// ).await?;
252    ///
253    /// // Fetch funding rate history from the last 7 days
254    /// let since = chrono::Utc::now().timestamp_millis() - 604800000;
255    /// let history = exchange.fetch_funding_rate_history(
256    ///     "BTC/USDT:USDT",
257    ///     Some(since),
258    ///     Some(50)
259    /// ).await?;
260    /// ```
261    async fn fetch_funding_rate_history(
262        &self,
263        symbol: &str,
264        since: Option<i64>,
265        limit: Option<u32>,
266    ) -> Result<Vec<FundingRateHistory>>;
267
268    // ========================================================================
269    // Deprecated u64 Wrapper Methods (Backward Compatibility)
270    // ========================================================================
271
272    /// Fetch funding rate history with u64 timestamp filtering (deprecated).
273    ///
274    /// **DEPRECATED**: Use `fetch_funding_rate_history` with i64 timestamps instead.
275    /// This method is provided for backward compatibility during migration.
276    ///
277    /// # Migration
278    ///
279    /// ```rust,ignore
280    /// // Old code (deprecated)
281    /// let history = exchange.fetch_funding_rate_history_u64("BTC/USDT:USDT", Some(1609459200000u64), Some(100)).await?;
282    ///
283    /// // New code (recommended)
284    /// let history = exchange.fetch_funding_rate_history("BTC/USDT:USDT", Some(1609459200000i64), Some(100)).await?;
285    /// ```
286    #[deprecated(
287        since = "0.1.0",
288        note = "Use fetch_funding_rate_history with i64 timestamps. Convert using TimestampUtils::u64_to_i64()"
289    )]
290    async fn fetch_funding_rate_history_u64(
291        &self,
292        symbol: &str,
293        since: Option<u64>,
294        limit: Option<u32>,
295    ) -> Result<Vec<FundingRateHistory>> {
296        use crate::time::TimestampConversion;
297
298        let since_i64 = since.to_i64()?;
299        self.fetch_funding_rate_history(symbol, since_i64, limit)
300            .await
301    }
302}
303
304/// Type alias for boxed Margin trait object.
305pub type BoxedMargin = Box<dyn Margin>;
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::capability::ExchangeCapabilities;
311    use crate::types::Timeframe;
312
313    // Mock implementation for testing trait object safety
314    struct MockExchange;
315
316    impl PublicExchange for MockExchange {
317        fn id(&self) -> &str {
318            "mock"
319        }
320        fn name(&self) -> &str {
321            "Mock Exchange"
322        }
323        fn capabilities(&self) -> ExchangeCapabilities {
324            ExchangeCapabilities::all()
325        }
326        fn timeframes(&self) -> Vec<Timeframe> {
327            vec![Timeframe::H1]
328        }
329    }
330
331    #[async_trait]
332    impl Margin for MockExchange {
333        async fn fetch_positions_for(&self, _symbols: &[&str]) -> Result<Vec<Position>> {
334            Ok(vec![Position {
335                symbol: "BTC/USDT:USDT".to_string(),
336                contracts: Some(1.0),
337                entry_price: Some(50000.0),
338                leverage: Some(10.0),
339                unrealized_pnl: Some(100.0),
340                ..Default::default()
341            }])
342        }
343
344        async fn fetch_position(&self, symbol: &str) -> Result<Position> {
345            Ok(Position {
346                symbol: symbol.to_string(),
347                contracts: Some(1.0),
348                entry_price: Some(50000.0),
349                leverage: Some(10.0),
350                ..Default::default()
351            })
352        }
353
354        async fn set_leverage_with_params(&self, _params: LeverageParams) -> Result<()> {
355            Ok(())
356        }
357
358        async fn get_leverage(&self, _symbol: &str) -> Result<u32> {
359            Ok(10)
360        }
361
362        async fn set_margin_mode(&self, _symbol: &str, _mode: MarginMode) -> Result<()> {
363            Ok(())
364        }
365
366        async fn fetch_funding_rate(&self, symbol: &str) -> Result<FundingRate> {
367            Ok(FundingRate {
368                symbol: symbol.to_string(),
369                funding_rate: Some(0.0001),
370                ..Default::default()
371            })
372        }
373
374        async fn fetch_funding_rates(&self, symbols: &[&str]) -> Result<Vec<FundingRate>> {
375            Ok(symbols
376                .iter()
377                .map(|s| FundingRate {
378                    symbol: s.to_string(),
379                    funding_rate: Some(0.0001),
380                    ..Default::default()
381                })
382                .collect())
383        }
384
385        async fn fetch_funding_rate_history(
386            &self,
387            symbol: &str,
388            _since: Option<i64>,
389            _limit: Option<u32>,
390        ) -> Result<Vec<FundingRateHistory>> {
391            Ok(vec![FundingRateHistory {
392                symbol: symbol.to_string(),
393                funding_rate: Some(0.0001),
394                timestamp: Some(1609459200000),
395                ..Default::default()
396            }])
397        }
398    }
399
400    #[test]
401    fn test_trait_object_safety() {
402        // Verify trait is object-safe by creating a trait object
403        let _exchange: BoxedMargin = Box::new(MockExchange);
404    }
405
406    #[tokio::test]
407    async fn test_fetch_positions() {
408        let exchange = MockExchange;
409
410        let positions = exchange.fetch_positions().await.unwrap();
411        assert_eq!(positions.len(), 1);
412        assert_eq!(positions[0].symbol, "BTC/USDT:USDT");
413    }
414
415    #[tokio::test]
416    async fn test_fetch_position() {
417        let exchange = MockExchange;
418
419        let position = exchange.fetch_position("BTC/USDT:USDT").await.unwrap();
420        assert_eq!(position.symbol, "BTC/USDT:USDT");
421        assert_eq!(position.contracts, Some(1.0));
422    }
423
424    #[tokio::test]
425    async fn test_set_leverage() {
426        let exchange = MockExchange;
427
428        let result = exchange.set_leverage("BTC/USDT:USDT", 10).await;
429        assert!(result.is_ok());
430    }
431
432    #[tokio::test]
433    async fn test_get_leverage() {
434        let exchange = MockExchange;
435
436        let leverage = exchange.get_leverage("BTC/USDT:USDT").await.unwrap();
437        assert_eq!(leverage, 10);
438    }
439
440    #[tokio::test]
441    async fn test_set_margin_mode() {
442        let exchange = MockExchange;
443
444        let result = exchange
445            .set_margin_mode("BTC/USDT:USDT", MarginMode::Isolated)
446            .await;
447        assert!(result.is_ok());
448    }
449
450    #[tokio::test]
451    async fn test_fetch_funding_rate() {
452        let exchange = MockExchange;
453
454        let rate = exchange.fetch_funding_rate("BTC/USDT:USDT").await.unwrap();
455        assert_eq!(rate.symbol, "BTC/USDT:USDT");
456        assert_eq!(rate.funding_rate, Some(0.0001));
457    }
458
459    #[tokio::test]
460    async fn test_fetch_funding_rates() {
461        let exchange = MockExchange;
462
463        let rates = exchange
464            .fetch_funding_rates(&["BTC/USDT:USDT", "ETH/USDT:USDT"])
465            .await
466            .unwrap();
467        assert_eq!(rates.len(), 2);
468    }
469
470    #[tokio::test]
471    async fn test_fetch_funding_rate_history() {
472        let exchange = MockExchange;
473
474        let history = exchange
475            .fetch_funding_rate_history("BTC/USDT:USDT", None, Some(100))
476            .await
477            .unwrap();
478        assert_eq!(history.len(), 1);
479        assert_eq!(history[0].funding_rate, Some(0.0001));
480    }
481}