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}