ccxt_core/exchange.rs
1//! # Unified Exchange Trait
2//!
3//! This module defines the core [`Exchange`] trait that all exchange implementations must implement.
4//! It provides a unified, polymorphic interface for interacting with cryptocurrency exchanges.
5//!
6//! ## Overview
7//!
8//! The `Exchange` trait is the central abstraction in CCXT-Rust. It enables:
9//!
10//! - **Polymorphic Exchange Usage**: Write exchange-agnostic trading code using `dyn Exchange`
11//! - **Capability Discovery**: Query exchange features at runtime via [`ExchangeCapabilities`]
12//! - **Type Safety**: Leverage Rust's type system for compile-time guarantees
13//! - **Thread Safety**: All implementations are `Send + Sync` for async runtime compatibility
14//!
15//! ## Architecture
16//!
17//! ```text
18//! ┌─────────────────────────────────────────────────────────────┐
19//! │ Exchange Trait │
20//! ├─────────────────────────────────────────────────────────────┤
21//! │ Metadata Methods │
22//! │ ├── id(), name(), version(), certified() │
23//! │ ├── capabilities(), timeframes(), rate_limit() │
24//! │ └── has_websocket() │
25//! ├─────────────────────────────────────────────────────────────┤
26//! │ Market Data Methods (Public API) │
27//! │ ├── fetch_markets(), load_markets() │
28//! │ ├── fetch_ticker(), fetch_tickers() │
29//! │ ├── fetch_order_book(), fetch_trades() │
30//! │ └── fetch_ohlcv() │
31//! ├─────────────────────────────────────────────────────────────┤
32//! │ Trading Methods (Private API) │
33//! │ ├── create_order(), cancel_order(), cancel_all_orders() │
34//! │ └── fetch_order(), fetch_open_orders(), fetch_closed_orders()│
35//! ├─────────────────────────────────────────────────────────────┤
36//! │ Account Methods (Private API) │
37//! │ └── fetch_balance(), fetch_my_trades() │
38//! └─────────────────────────────────────────────────────────────┘
39//! ```
40//!
41//! ## Key Types
42//!
43//! - [`Exchange`]: The core trait defining the unified exchange interface
44//! - [`ExchangeCapabilities`]: Describes which features an exchange supports
45//! - [`BoxedExchange`]: Type alias for `Box<dyn Exchange>` (owned trait object)
46//! - [`ArcExchange`]: Type alias for `Arc<dyn Exchange>` (shared trait object)
47//!
48//! ## Usage Examples
49//!
50//! ### Basic Exchange Usage
51//!
52//! ```rust,no_run
53//! use ccxt_core::exchange::{Exchange, ExchangeCapabilities};
54//!
55//! async fn print_exchange_info(exchange: &dyn Exchange) {
56//! println!("Exchange: {} ({})", exchange.name(), exchange.id());
57//! println!("Version: {}", exchange.version());
58//! println!("Certified: {}", exchange.certified());
59//! println!("Rate Limit: {} req/s", exchange.rate_limit());
60//! }
61//! ```
62//!
63//! ### Checking Capabilities Before Calling Methods
64//!
65//! ```rust,no_run
66//! use ccxt_core::exchange::{Exchange, ExchangeCapabilities};
67//!
68//! async fn safe_fetch_ticker(
69//! exchange: &dyn Exchange,
70//! symbol: &str,
71//! ) -> ccxt_core::Result<ccxt_core::Ticker> {
72//! // Always check capability before calling
73//! if !exchange.capabilities().fetch_ticker {
74//! return Err(ccxt_core::Error::not_implemented("fetch_ticker"));
75//! }
76//! exchange.fetch_ticker(symbol).await
77//! }
78//! ```
79//!
80//! ### Using Multiple Exchanges Polymorphically
81//!
82//! ```rust,no_run
83//! use ccxt_core::exchange::{Exchange, BoxedExchange};
84//! use ccxt_core::types::Price;
85//!
86//! async fn fetch_best_price(
87//! exchanges: &[BoxedExchange],
88//! symbol: &str,
89//! ) -> ccxt_core::Result<Price> {
90//! let mut best_price: Option<Price> = None;
91//!
92//! for exchange in exchanges {
93//! if exchange.capabilities().fetch_ticker {
94//! if let Ok(ticker) = exchange.fetch_ticker(symbol).await {
95//! if let Some(last) = ticker.last {
96//! best_price = Some(match best_price {
97//! None => last,
98//! Some(current) => if current < last { current } else { last },
99//! });
100//! }
101//! }
102//! }
103//! }
104//!
105//! best_price.ok_or_else(|| ccxt_core::Error::market_not_found("symbol"))
106//! }
107//! ```
108//!
109//! ### Thread-Safe Shared Exchange
110//!
111//! ```rust,no_run
112//! use ccxt_core::exchange::{Exchange, ArcExchange};
113//! use std::sync::Arc;
114//!
115//! async fn spawn_ticker_tasks(
116//! exchange: ArcExchange,
117//! symbols: Vec<String>,
118//! ) {
119//! let handles: Vec<_> = symbols
120//! .into_iter()
121//! .map(|symbol| {
122//! let ex = Arc::clone(&exchange);
123//! tokio::spawn(async move {
124//! ex.fetch_ticker(&symbol).await
125//! })
126//! })
127//! .collect();
128//!
129//! for handle in handles {
130//! let _ = handle.await;
131//! }
132//! }
133//! ```
134//!
135//! ## ExchangeCapabilities
136//!
137//! The [`ExchangeCapabilities`] struct provides runtime feature discovery:
138//!
139//! ```rust
140//! use ccxt_core::exchange::ExchangeCapabilities;
141//!
142//! // Create capabilities for public-only access
143//! let public_caps = ExchangeCapabilities::public_only();
144//! assert!(public_caps.fetch_ticker);
145//! assert!(!public_caps.create_order);
146//!
147//! // Create capabilities with all features
148//! let all_caps = ExchangeCapabilities::all();
149//! assert!(all_caps.create_order);
150//! assert!(all_caps.websocket);
151//!
152//! // Check capability by name (CCXT-style camelCase)
153//! assert!(all_caps.has("fetchTicker"));
154//! assert!(all_caps.has("createOrder"));
155//! ```
156//!
157//! ## Error Handling
158//!
159//! All exchange methods return `Result<T>` with comprehensive error types:
160//!
161//! - `NotImplemented`: Method not supported by this exchange
162//! - `Authentication`: API credentials missing or invalid
163//! - `RateLimit`: Too many requests
164//! - `Network`: Connection or timeout errors
165//! - `Exchange`: Exchange-specific errors
166//!
167//! ## Thread Safety
168//!
169//! The `Exchange` trait requires `Send + Sync` bounds, ensuring:
170//!
171//! - Exchanges can be sent across thread boundaries (`Send`)
172//! - Exchanges can be shared across threads via `Arc` (`Sync`)
173//! - Compatible with Tokio and other async runtimes
174//!
175//! ## See Also
176//!
177//! - [`crate::ws_exchange::WsExchange`]: WebSocket streaming trait
178//! - [`crate::ws_exchange::FullExchange`]: Combined REST + WebSocket trait
179//! - [`crate::base_exchange::BaseExchange`]: Base implementation utilities
180
181use async_trait::async_trait;
182use rust_decimal::Decimal;
183use std::collections::HashMap;
184use std::sync::Arc;
185
186use crate::error::Result;
187use crate::types::*;
188
189// ============================================================================
190// ExchangeCapabilities
191// ============================================================================
192
193/// Exchange capabilities - describes what features an exchange supports
194///
195/// This struct contains boolean flags for each capability that an exchange
196/// may or may not support. Use this to check feature availability before
197/// calling methods that may not be implemented.
198///
199/// # Example
200///
201/// ```rust
202/// use ccxt_core::exchange::ExchangeCapabilities;
203///
204/// let caps = ExchangeCapabilities::public_only();
205/// assert!(caps.fetch_ticker);
206/// assert!(!caps.create_order);
207/// ```
208#[derive(Debug, Clone, Default, PartialEq, Eq)]
209pub struct ExchangeCapabilities {
210 // ==================== Market Data (Public API) ====================
211 /// Can fetch market definitions
212 pub fetch_markets: bool,
213 /// Can fetch currency definitions
214 pub fetch_currencies: bool,
215 /// Can fetch single ticker
216 pub fetch_ticker: bool,
217 /// Can fetch multiple tickers
218 pub fetch_tickers: bool,
219 /// Can fetch order book
220 pub fetch_order_book: bool,
221 /// Can fetch public trades
222 pub fetch_trades: bool,
223 /// Can fetch OHLCV candlestick data
224 pub fetch_ohlcv: bool,
225 /// Can fetch exchange status
226 pub fetch_status: bool,
227 /// Can fetch server time
228 pub fetch_time: bool,
229
230 // ==================== Trading (Private API) ====================
231 /// Can create orders
232 pub create_order: bool,
233 /// Can create market orders
234 pub create_market_order: bool,
235 /// Can create limit orders
236 pub create_limit_order: bool,
237 /// Can cancel orders
238 pub cancel_order: bool,
239 /// Can cancel all orders
240 pub cancel_all_orders: bool,
241 /// Can edit/modify orders
242 pub edit_order: bool,
243 /// Can fetch single order
244 pub fetch_order: bool,
245 /// Can fetch all orders
246 pub fetch_orders: bool,
247 /// Can fetch open orders
248 pub fetch_open_orders: bool,
249 /// Can fetch closed orders
250 pub fetch_closed_orders: bool,
251 /// Can fetch canceled orders
252 pub fetch_canceled_orders: bool,
253
254 // ==================== Account (Private API) ====================
255 /// Can fetch account balance
256 pub fetch_balance: bool,
257 /// Can fetch user's trade history
258 pub fetch_my_trades: bool,
259 /// Can fetch deposit history
260 pub fetch_deposits: bool,
261 /// Can fetch withdrawal history
262 pub fetch_withdrawals: bool,
263 /// Can fetch transaction history
264 pub fetch_transactions: bool,
265 /// Can fetch ledger entries
266 pub fetch_ledger: bool,
267
268 // ==================== Funding ====================
269 /// Can fetch deposit address
270 pub fetch_deposit_address: bool,
271 /// Can create deposit address
272 pub create_deposit_address: bool,
273 /// Can withdraw funds
274 pub withdraw: bool,
275 /// Can transfer between accounts
276 pub transfer: bool,
277
278 // ==================== Margin Trading ====================
279 /// Can fetch borrow rate
280 pub fetch_borrow_rate: bool,
281 /// Can fetch multiple borrow rates
282 pub fetch_borrow_rates: bool,
283 /// Can fetch funding rate
284 pub fetch_funding_rate: bool,
285 /// Can fetch multiple funding rates
286 pub fetch_funding_rates: bool,
287 /// Can fetch positions
288 pub fetch_positions: bool,
289 /// Can set leverage
290 pub set_leverage: bool,
291 /// Can set margin mode
292 pub set_margin_mode: bool,
293
294 // ==================== WebSocket ====================
295 /// WebSocket support available
296 pub websocket: bool,
297 /// Can watch ticker updates
298 pub watch_ticker: bool,
299 /// Can watch multiple ticker updates
300 pub watch_tickers: bool,
301 /// Can watch order book updates
302 pub watch_order_book: bool,
303 /// Can watch trade updates
304 pub watch_trades: bool,
305 /// Can watch OHLCV updates
306 pub watch_ohlcv: bool,
307 /// Can watch balance updates
308 pub watch_balance: bool,
309 /// Can watch order updates
310 pub watch_orders: bool,
311 /// Can watch user trade updates
312 pub watch_my_trades: bool,
313}
314
315impl ExchangeCapabilities {
316 /// Create capabilities with all features enabled
317 ///
318 /// # Example
319 ///
320 /// ```rust
321 /// use ccxt_core::exchange::ExchangeCapabilities;
322 ///
323 /// let caps = ExchangeCapabilities::all();
324 /// assert!(caps.fetch_ticker);
325 /// assert!(caps.create_order);
326 /// assert!(caps.websocket);
327 /// ```
328 pub fn all() -> Self {
329 Self {
330 fetch_markets: true,
331 fetch_currencies: true,
332 fetch_ticker: true,
333 fetch_tickers: true,
334 fetch_order_book: true,
335 fetch_trades: true,
336 fetch_ohlcv: true,
337 fetch_status: true,
338 fetch_time: true,
339 create_order: true,
340 create_market_order: true,
341 create_limit_order: true,
342 cancel_order: true,
343 cancel_all_orders: true,
344 edit_order: true,
345 fetch_order: true,
346 fetch_orders: true,
347 fetch_open_orders: true,
348 fetch_closed_orders: true,
349 fetch_canceled_orders: true,
350 fetch_balance: true,
351 fetch_my_trades: true,
352 fetch_deposits: true,
353 fetch_withdrawals: true,
354 fetch_transactions: true,
355 fetch_ledger: true,
356 fetch_deposit_address: true,
357 create_deposit_address: true,
358 withdraw: true,
359 transfer: true,
360 fetch_borrow_rate: true,
361 fetch_borrow_rates: true,
362 fetch_funding_rate: true,
363 fetch_funding_rates: true,
364 fetch_positions: true,
365 set_leverage: true,
366 set_margin_mode: true,
367 websocket: true,
368 watch_ticker: true,
369 watch_tickers: true,
370 watch_order_book: true,
371 watch_trades: true,
372 watch_ohlcv: true,
373 watch_balance: true,
374 watch_orders: true,
375 watch_my_trades: true,
376 }
377 }
378
379 /// Create capabilities for public API only (no authentication required)
380 ///
381 /// # Example
382 ///
383 /// ```rust
384 /// use ccxt_core::exchange::ExchangeCapabilities;
385 ///
386 /// let caps = ExchangeCapabilities::public_only();
387 /// assert!(caps.fetch_ticker);
388 /// assert!(!caps.create_order);
389 /// ```
390 pub fn public_only() -> Self {
391 Self {
392 fetch_markets: true,
393 fetch_currencies: true,
394 fetch_ticker: true,
395 fetch_tickers: true,
396 fetch_order_book: true,
397 fetch_trades: true,
398 fetch_ohlcv: true,
399 fetch_status: true,
400 fetch_time: true,
401 ..Default::default()
402 }
403 }
404
405 /// Check if a capability is supported by name
406 ///
407 /// This method allows checking capabilities using CCXT-style camelCase names.
408 ///
409 /// # Arguments
410 ///
411 /// * `capability` - The capability name in camelCase format
412 ///
413 /// # Example
414 ///
415 /// ```rust
416 /// use ccxt_core::exchange::ExchangeCapabilities;
417 ///
418 /// let caps = ExchangeCapabilities::all();
419 /// assert!(caps.has("fetchTicker"));
420 /// assert!(caps.has("createOrder"));
421 /// assert!(!caps.has("unknownCapability"));
422 /// ```
423 pub fn has(&self, capability: &str) -> bool {
424 match capability {
425 "fetchMarkets" => self.fetch_markets,
426 "fetchCurrencies" => self.fetch_currencies,
427 "fetchTicker" => self.fetch_ticker,
428 "fetchTickers" => self.fetch_tickers,
429 "fetchOrderBook" => self.fetch_order_book,
430 "fetchTrades" => self.fetch_trades,
431 "fetchOHLCV" => self.fetch_ohlcv,
432 "fetchStatus" => self.fetch_status,
433 "fetchTime" => self.fetch_time,
434 "createOrder" => self.create_order,
435 "createMarketOrder" => self.create_market_order,
436 "createLimitOrder" => self.create_limit_order,
437 "cancelOrder" => self.cancel_order,
438 "cancelAllOrders" => self.cancel_all_orders,
439 "editOrder" => self.edit_order,
440 "fetchOrder" => self.fetch_order,
441 "fetchOrders" => self.fetch_orders,
442 "fetchOpenOrders" => self.fetch_open_orders,
443 "fetchClosedOrders" => self.fetch_closed_orders,
444 "fetchCanceledOrders" => self.fetch_canceled_orders,
445 "fetchBalance" => self.fetch_balance,
446 "fetchMyTrades" => self.fetch_my_trades,
447 "fetchDeposits" => self.fetch_deposits,
448 "fetchWithdrawals" => self.fetch_withdrawals,
449 "fetchTransactions" => self.fetch_transactions,
450 "fetchLedger" => self.fetch_ledger,
451 "fetchDepositAddress" => self.fetch_deposit_address,
452 "createDepositAddress" => self.create_deposit_address,
453 "withdraw" => self.withdraw,
454 "transfer" => self.transfer,
455 "fetchBorrowRate" => self.fetch_borrow_rate,
456 "fetchBorrowRates" => self.fetch_borrow_rates,
457 "fetchFundingRate" => self.fetch_funding_rate,
458 "fetchFundingRates" => self.fetch_funding_rates,
459 "fetchPositions" => self.fetch_positions,
460 "setLeverage" => self.set_leverage,
461 "setMarginMode" => self.set_margin_mode,
462 "websocket" => self.websocket,
463 "watchTicker" => self.watch_ticker,
464 "watchTickers" => self.watch_tickers,
465 "watchOrderBook" => self.watch_order_book,
466 "watchTrades" => self.watch_trades,
467 "watchOHLCV" => self.watch_ohlcv,
468 "watchBalance" => self.watch_balance,
469 "watchOrders" => self.watch_orders,
470 "watchMyTrades" => self.watch_my_trades,
471 _ => false,
472 }
473 }
474
475 /// Get a list of all supported capability names
476 pub fn supported_capabilities(&self) -> Vec<&'static str> {
477 let mut caps = Vec::new();
478 if self.fetch_markets {
479 caps.push("fetchMarkets");
480 }
481 if self.fetch_currencies {
482 caps.push("fetchCurrencies");
483 }
484 if self.fetch_ticker {
485 caps.push("fetchTicker");
486 }
487 if self.fetch_tickers {
488 caps.push("fetchTickers");
489 }
490 if self.fetch_order_book {
491 caps.push("fetchOrderBook");
492 }
493 if self.fetch_trades {
494 caps.push("fetchTrades");
495 }
496 if self.fetch_ohlcv {
497 caps.push("fetchOHLCV");
498 }
499 if self.fetch_status {
500 caps.push("fetchStatus");
501 }
502 if self.fetch_time {
503 caps.push("fetchTime");
504 }
505 if self.create_order {
506 caps.push("createOrder");
507 }
508 if self.create_market_order {
509 caps.push("createMarketOrder");
510 }
511 if self.create_limit_order {
512 caps.push("createLimitOrder");
513 }
514 if self.cancel_order {
515 caps.push("cancelOrder");
516 }
517 if self.cancel_all_orders {
518 caps.push("cancelAllOrders");
519 }
520 if self.edit_order {
521 caps.push("editOrder");
522 }
523 if self.fetch_order {
524 caps.push("fetchOrder");
525 }
526 if self.fetch_orders {
527 caps.push("fetchOrders");
528 }
529 if self.fetch_open_orders {
530 caps.push("fetchOpenOrders");
531 }
532 if self.fetch_closed_orders {
533 caps.push("fetchClosedOrders");
534 }
535 if self.fetch_canceled_orders {
536 caps.push("fetchCanceledOrders");
537 }
538 if self.fetch_balance {
539 caps.push("fetchBalance");
540 }
541 if self.fetch_my_trades {
542 caps.push("fetchMyTrades");
543 }
544 if self.websocket {
545 caps.push("websocket");
546 }
547 caps
548 }
549}
550
551// ============================================================================
552// Exchange Trait
553// ============================================================================
554
555/// Core Exchange trait - the unified interface for all exchanges
556///
557/// This trait defines the standard API that all exchange implementations
558/// must provide. It is designed to be object-safe for dynamic dispatch,
559/// allowing exchanges to be used polymorphically via `dyn Exchange`.
560///
561/// # Thread Safety
562///
563/// All implementations must be `Send + Sync` to allow safe usage across
564/// thread boundaries.
565///
566/// # Example
567///
568/// ```rust,no_run
569/// use ccxt_core::exchange::Exchange;
570///
571/// async fn print_exchange_info(exchange: &dyn Exchange) {
572/// println!("Exchange: {} ({})", exchange.name(), exchange.id());
573/// println!("Version: {}", exchange.version());
574/// println!("Certified: {}", exchange.certified());
575/// }
576/// ```
577#[async_trait]
578pub trait Exchange: Send + Sync {
579 // ==================== Metadata ====================
580
581 /// Returns the exchange identifier (e.g., "binance", "coinbase")
582 ///
583 /// This is a lowercase, URL-safe identifier used internally.
584 fn id(&self) -> &str;
585
586 /// Returns the human-readable exchange name (e.g., "Binance", "Coinbase")
587 fn name(&self) -> &str;
588
589 /// Returns the API version string
590 fn version(&self) -> &'static str {
591 "1.0.0"
592 }
593
594 /// Returns whether this exchange is CCXT certified
595 ///
596 /// Certified exchanges have been thoroughly tested and verified.
597 fn certified(&self) -> bool {
598 false
599 }
600
601 /// Returns whether this exchange supports WebSocket (pro features)
602 fn has_websocket(&self) -> bool {
603 self.capabilities().websocket
604 }
605
606 /// Returns the exchange capabilities
607 ///
608 /// Use this to check which features are supported before calling methods.
609 fn capabilities(&self) -> ExchangeCapabilities;
610
611 /// Returns supported timeframes for OHLCV data
612 fn timeframes(&self) -> Vec<Timeframe> {
613 vec![
614 Timeframe::M1,
615 Timeframe::M5,
616 Timeframe::M15,
617 Timeframe::H1,
618 Timeframe::H4,
619 Timeframe::D1,
620 ]
621 }
622
623 /// Returns the rate limit (requests per second)
624 fn rate_limit(&self) -> f64 {
625 10.0
626 }
627
628 // ==================== Market Data (Public API) ====================
629
630 /// Fetch all available markets
631 ///
632 /// # Returns
633 ///
634 /// A vector of `Market` structs containing market definitions.
635 ///
636 /// # Errors
637 ///
638 /// Returns an error if the request fails or the exchange is unavailable.
639 async fn fetch_markets(&self) -> Result<Vec<Market>>;
640
641 /// Load markets and cache them
642 ///
643 /// # Arguments
644 ///
645 /// * `reload` - If true, force reload even if markets are cached
646 ///
647 /// # Returns
648 ///
649 /// A HashMap of markets indexed by symbol.
650 async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>>;
651
652 /// Fetch ticker for a single symbol
653 ///
654 /// # Arguments
655 ///
656 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
657 ///
658 /// # Returns
659 ///
660 /// The ticker data for the specified symbol.
661 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker>;
662
663 /// Fetch tickers for multiple symbols (or all if None)
664 ///
665 /// # Arguments
666 ///
667 /// * `symbols` - Optional list of symbols to fetch. If None, fetches all.
668 ///
669 /// # Returns
670 ///
671 /// A vector of tickers.
672 async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>>;
673
674 /// Fetch order book for a symbol
675 ///
676 /// # Arguments
677 ///
678 /// * `symbol` - Trading pair symbol
679 /// * `limit` - Optional limit on the number of orders per side
680 ///
681 /// # Returns
682 ///
683 /// The order book containing bids and asks.
684 async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook>;
685
686 /// Fetch recent public trades
687 ///
688 /// # Arguments
689 ///
690 /// * `symbol` - Trading pair symbol
691 /// * `limit` - Optional limit on the number of trades
692 ///
693 /// # Returns
694 ///
695 /// A vector of recent trades.
696 async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>>;
697
698 /// Fetch OHLCV candlestick data
699 ///
700 /// # Arguments
701 ///
702 /// * `symbol` - Trading pair symbol
703 /// * `timeframe` - Candlestick timeframe
704 /// * `since` - Optional start timestamp in milliseconds
705 /// * `limit` - Optional limit on the number of candles
706 ///
707 /// # Returns
708 ///
709 /// A vector of OHLCV candles.
710 async fn fetch_ohlcv(
711 &self,
712 symbol: &str,
713 timeframe: Timeframe,
714 since: Option<i64>,
715 limit: Option<u32>,
716 ) -> Result<Vec<Ohlcv>>;
717
718 // ==================== Trading (Private API) ====================
719
720 /// Create a new order
721 ///
722 /// # Arguments
723 ///
724 /// * `symbol` - Trading pair symbol
725 /// * `order_type` - Order type (limit, market, etc.)
726 /// * `side` - Order side (buy or sell)
727 /// * `amount` - Order amount
728 /// * `price` - Optional price (required for limit orders)
729 ///
730 /// # Returns
731 ///
732 /// The created order.
733 ///
734 /// # Errors
735 ///
736 /// Returns an error if authentication fails or the order is invalid.
737 async fn create_order(
738 &self,
739 symbol: &str,
740 order_type: OrderType,
741 side: OrderSide,
742 amount: Decimal,
743 price: Option<Decimal>,
744 ) -> Result<Order>;
745
746 /// Cancel an existing order
747 ///
748 /// # Arguments
749 ///
750 /// * `id` - Order ID to cancel
751 /// * `symbol` - Optional symbol (required by some exchanges)
752 ///
753 /// # Returns
754 ///
755 /// The canceled order.
756 async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order>;
757
758 /// Cancel all orders (optionally for a specific symbol)
759 ///
760 /// # Arguments
761 ///
762 /// * `symbol` - Optional symbol to cancel orders for
763 ///
764 /// # Returns
765 ///
766 /// A vector of canceled orders.
767 async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>>;
768
769 /// Fetch a specific order by ID
770 ///
771 /// # Arguments
772 ///
773 /// * `id` - Order ID
774 /// * `symbol` - Optional symbol (required by some exchanges)
775 ///
776 /// # Returns
777 ///
778 /// The order details.
779 async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order>;
780
781 /// Fetch all open orders
782 ///
783 /// # Arguments
784 ///
785 /// * `symbol` - Optional symbol to filter by
786 /// * `since` - Optional start timestamp
787 /// * `limit` - Optional limit on results
788 ///
789 /// # Returns
790 ///
791 /// A vector of open orders.
792 async fn fetch_open_orders(
793 &self,
794 symbol: Option<&str>,
795 since: Option<i64>,
796 limit: Option<u32>,
797 ) -> Result<Vec<Order>>;
798
799 /// Fetch closed orders
800 ///
801 /// # Arguments
802 ///
803 /// * `symbol` - Optional symbol to filter by
804 /// * `since` - Optional start timestamp
805 /// * `limit` - Optional limit on results
806 ///
807 /// # Returns
808 ///
809 /// A vector of closed orders.
810 async fn fetch_closed_orders(
811 &self,
812 symbol: Option<&str>,
813 since: Option<i64>,
814 limit: Option<u32>,
815 ) -> Result<Vec<Order>>;
816
817 // ==================== Account (Private API) ====================
818
819 /// Fetch account balance
820 ///
821 /// # Returns
822 ///
823 /// The account balance containing all currencies.
824 async fn fetch_balance(&self) -> Result<Balance>;
825
826 /// Fetch user's trade history
827 ///
828 /// # Arguments
829 ///
830 /// * `symbol` - Optional symbol to filter by
831 /// * `since` - Optional start timestamp
832 /// * `limit` - Optional limit on results
833 ///
834 /// # Returns
835 ///
836 /// A vector of user's trades.
837 async fn fetch_my_trades(
838 &self,
839 symbol: Option<&str>,
840 since: Option<i64>,
841 limit: Option<u32>,
842 ) -> Result<Vec<Trade>>;
843
844 // ==================== Helper Methods ====================
845
846 /// Get a specific market by symbol
847 ///
848 /// # Arguments
849 ///
850 /// * `symbol` - Trading pair symbol
851 ///
852 /// # Returns
853 ///
854 /// The market definition.
855 ///
856 /// # Errors
857 ///
858 /// Returns an error if the market is not found or markets are not loaded.
859 async fn market(&self, symbol: &str) -> Result<Market>;
860
861 /// Get all cached markets
862 ///
863 /// # Returns
864 ///
865 /// A HashMap of all markets indexed by symbol.
866 async fn markets(&self) -> HashMap<String, Market>;
867
868 /// Check if a symbol is valid and active
869 ///
870 /// # Arguments
871 ///
872 /// * `symbol` - Trading pair symbol
873 ///
874 /// # Returns
875 ///
876 /// True if the symbol exists and is active.
877 async fn is_symbol_active(&self, symbol: &str) -> bool {
878 self.market(symbol).await.map(|m| m.active).unwrap_or(false)
879 }
880}
881
882// ============================================================================
883// Type Aliases
884// ============================================================================
885
886/// Type alias for a boxed Exchange trait object
887///
888/// Use this when you need owned, heap-allocated exchange instances.
889pub type BoxedExchange = Box<dyn Exchange>;
890
891/// Type alias for an Arc-wrapped Exchange trait object
892///
893/// Use this when you need shared ownership across threads.
894pub type ArcExchange = Arc<dyn Exchange>;
895
896// ============================================================================
897// Tests
898// ============================================================================
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903
904 #[test]
905 fn test_capabilities_default() {
906 let caps = ExchangeCapabilities::default();
907 assert!(!caps.fetch_ticker);
908 assert!(!caps.create_order);
909 assert!(!caps.websocket);
910 }
911
912 #[test]
913 fn test_capabilities_all() {
914 let caps = ExchangeCapabilities::all();
915 assert!(caps.fetch_ticker);
916 assert!(caps.create_order);
917 assert!(caps.websocket);
918 assert!(caps.fetch_ohlcv);
919 assert!(caps.fetch_balance);
920 }
921
922 #[test]
923 fn test_capabilities_public_only() {
924 let caps = ExchangeCapabilities::public_only();
925 assert!(caps.fetch_ticker);
926 assert!(caps.fetch_order_book);
927 assert!(caps.fetch_trades);
928 assert!(!caps.create_order);
929 assert!(!caps.fetch_balance);
930 assert!(!caps.websocket);
931 }
932
933 #[test]
934 fn test_capabilities_has() {
935 let caps = ExchangeCapabilities::all();
936 assert!(caps.has("fetchTicker"));
937 assert!(caps.has("createOrder"));
938 assert!(caps.has("websocket"));
939 assert!(!caps.has("unknownCapability"));
940 }
941
942 #[test]
943 fn test_capabilities_supported_list() {
944 let caps = ExchangeCapabilities::public_only();
945 let supported = caps.supported_capabilities();
946 assert!(supported.contains(&"fetchTicker"));
947 assert!(supported.contains(&"fetchOrderBook"));
948 assert!(!supported.contains(&"createOrder"));
949 }
950
951 #[test]
952 fn test_capabilities_equality() {
953 let caps1 = ExchangeCapabilities::all();
954 let caps2 = ExchangeCapabilities::all();
955 assert_eq!(caps1, caps2);
956
957 let caps3 = ExchangeCapabilities::public_only();
958 assert_ne!(caps1, caps3);
959 }
960}
961
962// ============================================================================
963// Property-Based Tests
964// ============================================================================
965
966#[cfg(test)]
967mod property_tests {
968 use super::*;
969 use crate::error::Error;
970 use proptest::prelude::*;
971 use std::thread;
972
973 // ==================== Strategies ====================
974
975 /// Strategy to generate arbitrary ExchangeCapabilities
976 fn arb_capabilities() -> impl Strategy<Value = ExchangeCapabilities> {
977 prop_oneof![
978 Just(ExchangeCapabilities::default()),
979 Just(ExchangeCapabilities::all()),
980 Just(ExchangeCapabilities::public_only()),
981 // Random capabilities
982 (
983 prop::bool::ANY,
984 prop::bool::ANY,
985 prop::bool::ANY,
986 prop::bool::ANY,
987 prop::bool::ANY,
988 prop::bool::ANY,
989 )
990 .prop_map(
991 |(
992 fetch_ticker,
993 fetch_order_book,
994 create_order,
995 websocket,
996 fetch_balance,
997 fetch_ohlcv,
998 )| {
999 ExchangeCapabilities {
1000 fetch_ticker,
1001 fetch_order_book,
1002 create_order,
1003 websocket,
1004 fetch_balance,
1005 fetch_ohlcv,
1006 ..Default::default()
1007 }
1008 }
1009 ),
1010 ]
1011 }
1012
1013 /// Strategy to generate arbitrary error messages
1014 fn arb_error_message() -> impl Strategy<Value = String> {
1015 prop_oneof![
1016 Just("".to_string()),
1017 "[a-zA-Z0-9 .,!?-]{1,100}",
1018 // Unicode messages
1019 "\\PC{1,50}",
1020 ]
1021 }
1022
1023 /// Strategy to generate arbitrary Error variants for testing error propagation
1024 fn arb_error() -> impl Strategy<Value = Error> {
1025 prop_oneof![
1026 // Authentication errors
1027 arb_error_message().prop_map(|msg| Error::authentication(msg)),
1028 // Invalid request errors
1029 arb_error_message().prop_map(|msg| Error::invalid_request(msg)),
1030 // Market not found errors
1031 arb_error_message().prop_map(|msg| Error::market_not_found(msg)),
1032 // Timeout errors
1033 arb_error_message().prop_map(|msg| Error::timeout(msg)),
1034 // Not implemented errors
1035 arb_error_message().prop_map(|msg| Error::not_implemented(msg)),
1036 // Network errors
1037 arb_error_message().prop_map(|msg| Error::network(msg)),
1038 // WebSocket errors
1039 arb_error_message().prop_map(|msg| Error::websocket(msg)),
1040 ]
1041 }
1042
1043 // ==================== Property 3: Thread Safety ====================
1044
1045 proptest! {
1046 #![proptest_config(ProptestConfig::with_cases(100))]
1047
1048 /// **Feature: unified-exchange-trait, Property 3: Thread Safety**
1049 ///
1050 /// *For any* exchange trait object, it should be possible to send it across
1051 /// thread boundaries (`Send`) and share references across threads (`Sync`).
1052 ///
1053 /// **Validates: Requirements 5.3**
1054 #[test]
1055 fn prop_exchange_capabilities_send_sync(caps in arb_capabilities()) {
1056 // Compile-time assertion: ExchangeCapabilities must be Send + Sync
1057 fn assert_send_sync<T: Send + Sync>(_: &T) {}
1058 assert_send_sync(&caps);
1059
1060 // Runtime verification: ExchangeCapabilities can be sent across threads
1061 let caps_clone = caps.clone();
1062 let handle = thread::spawn(move || {
1063 // Capabilities were successfully moved to another thread (Send)
1064 caps_clone.fetch_ticker
1065 });
1066 let result = handle.join().expect("Thread should not panic");
1067 prop_assert_eq!(result, caps.fetch_ticker);
1068 }
1069
1070 /// **Feature: unified-exchange-trait, Property 3: Thread Safety (Arc sharing)**
1071 ///
1072 /// *For any* ExchangeCapabilities, wrapping in Arc should allow safe sharing
1073 /// across multiple threads simultaneously.
1074 ///
1075 /// **Validates: Requirements 5.3**
1076 #[test]
1077 fn prop_exchange_capabilities_arc_sharing(caps in arb_capabilities()) {
1078 use std::sync::Arc;
1079
1080 let shared_caps = Arc::new(caps.clone());
1081
1082 // Spawn multiple threads that read from the shared capabilities
1083 let handles: Vec<_> = (0..4)
1084 .map(|_| {
1085 let caps_ref = Arc::clone(&shared_caps);
1086 thread::spawn(move || {
1087 // Read various capabilities from different threads
1088 (
1089 caps_ref.fetch_ticker,
1090 caps_ref.create_order,
1091 caps_ref.websocket,
1092 )
1093 })
1094 })
1095 .collect();
1096
1097 // All threads should complete successfully with consistent values
1098 for handle in handles {
1099 let (fetch_ticker, create_order, websocket) =
1100 handle.join().expect("Thread should not panic");
1101 prop_assert_eq!(fetch_ticker, caps.fetch_ticker);
1102 prop_assert_eq!(create_order, caps.create_order);
1103 prop_assert_eq!(websocket, caps.websocket);
1104 }
1105 }
1106
1107 /// **Feature: unified-exchange-trait, Property 3: Thread Safety (BoxedExchange type alias)**
1108 ///
1109 /// Verifies that the BoxedExchange type alias (Box<dyn Exchange>) satisfies
1110 /// Send + Sync bounds required for async runtime usage.
1111 ///
1112 /// **Validates: Requirements 5.3**
1113 #[test]
1114 fn prop_boxed_exchange_type_is_send_sync(_dummy in Just(())) {
1115 // Compile-time assertion: BoxedExchange must be Send
1116 fn assert_send<T: Send>() {}
1117 assert_send::<BoxedExchange>();
1118
1119 // Note: Box<dyn Exchange> is Send because Exchange: Send + Sync
1120 // This is a compile-time check that validates the trait bounds
1121 prop_assert!(true, "BoxedExchange type satisfies Send bound");
1122 }
1123
1124 /// **Feature: unified-exchange-trait, Property 3: Thread Safety (ArcExchange type alias)**
1125 ///
1126 /// Verifies that the ArcExchange type alias (Arc<dyn Exchange>) satisfies
1127 /// Send + Sync bounds required for shared ownership across threads.
1128 ///
1129 /// **Validates: Requirements 5.3**
1130 #[test]
1131 fn prop_arc_exchange_type_is_send_sync(_dummy in Just(())) {
1132 // Compile-time assertion: ArcExchange must be Send + Sync
1133 fn assert_send_sync<T: Send + Sync>() {}
1134 assert_send_sync::<ArcExchange>();
1135
1136 // This is a compile-time check that validates the trait bounds
1137 prop_assert!(true, "ArcExchange type satisfies Send + Sync bounds");
1138 }
1139 }
1140
1141 // ==================== Property 4: Error Propagation ====================
1142
1143 proptest! {
1144 #![proptest_config(ProptestConfig::with_cases(100))]
1145
1146 /// **Feature: unified-exchange-trait, Property 4: Error Propagation**
1147 ///
1148 /// *For any* async method call that fails, the error should be properly
1149 /// propagated through the `Result` type without panicking.
1150 ///
1151 /// **Validates: Requirements 5.4**
1152 #[test]
1153 fn prop_error_propagation_through_result(error in arb_error()) {
1154 // Store the error string before moving
1155 let error_string = error.to_string();
1156
1157 // Create a Result with the error
1158 let result: Result<()> = Err(error);
1159
1160 // Verify error can be extracted without panicking
1161 prop_assert!(result.is_err());
1162
1163 let extracted_error = result.unwrap_err();
1164
1165 // Error should preserve its display message
1166 prop_assert_eq!(
1167 extracted_error.to_string(),
1168 error_string,
1169 "Error display should be preserved"
1170 );
1171 }
1172
1173 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (with context)**
1174 ///
1175 /// *For any* error with context attached, the context should be preserved
1176 /// and the error chain should be traversable.
1177 ///
1178 /// **Validates: Requirements 5.4**
1179 #[test]
1180 fn prop_error_propagation_with_context(
1181 base_error in arb_error(),
1182 context in "[a-zA-Z0-9 ]{1,50}"
1183 ) {
1184 // Add context to the error
1185 let error_with_context = base_error.context(context.clone());
1186
1187 // The error display should contain the context
1188 let display = error_with_context.to_string();
1189 prop_assert!(
1190 display.contains(&context),
1191 "Error display '{}' should contain context '{}'",
1192 display,
1193 context
1194 );
1195
1196 // Error should still be usable in Result
1197 let result: Result<()> = Err(error_with_context);
1198 prop_assert!(result.is_err());
1199 }
1200
1201 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (Send + Sync)**
1202 ///
1203 /// *For any* error, it should be possible to send it across thread boundaries,
1204 /// which is essential for async error propagation.
1205 ///
1206 /// **Validates: Requirements 5.4**
1207 #[test]
1208 fn prop_error_send_across_threads(error in arb_error()) {
1209 // Compile-time assertion: Error must be Send + Sync
1210 fn assert_send_sync<T: Send + Sync + 'static>(_: &T) {}
1211 assert_send_sync(&error);
1212
1213 // Runtime verification: Error can be sent across threads
1214 let error_string = error.to_string();
1215 let handle = thread::spawn(move || {
1216 // Error was successfully moved to another thread (Send)
1217 error.to_string()
1218 });
1219 let result = handle.join().expect("Thread should not panic");
1220 prop_assert_eq!(result, error_string);
1221 }
1222
1223 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (Result chain)**
1224 ///
1225 /// *For any* sequence of operations that may fail, errors should propagate
1226 /// correctly through the ? operator pattern.
1227 ///
1228 /// **Validates: Requirements 5.4**
1229 #[test]
1230 fn prop_error_propagation_chain(
1231 error_msg in arb_error_message(),
1232 should_fail_first in prop::bool::ANY,
1233 should_fail_second in prop::bool::ANY
1234 ) {
1235 fn operation_one(fail: bool, msg: &str) -> Result<i32> {
1236 if fail {
1237 Err(Error::invalid_request(msg.to_string()))
1238 } else {
1239 Ok(42)
1240 }
1241 }
1242
1243 fn operation_two(fail: bool, msg: &str, input: i32) -> Result<i32> {
1244 if fail {
1245 Err(Error::invalid_request(msg.to_string()))
1246 } else {
1247 Ok(input * 2)
1248 }
1249 }
1250
1251 fn chained_operations(
1252 fail_first: bool,
1253 fail_second: bool,
1254 msg: &str,
1255 ) -> Result<i32> {
1256 let result = operation_one(fail_first, msg)?;
1257 operation_two(fail_second, msg, result)
1258 }
1259
1260 let result = chained_operations(should_fail_first, should_fail_second, &error_msg);
1261
1262 // Verify the result matches expected behavior
1263 if should_fail_first {
1264 prop_assert!(result.is_err(), "Should fail on first operation");
1265 } else if should_fail_second {
1266 prop_assert!(result.is_err(), "Should fail on second operation");
1267 } else {
1268 prop_assert!(result.is_ok(), "Should succeed when no failures");
1269 prop_assert_eq!(result.unwrap(), 84, "Result should be 42 * 2 = 84");
1270 }
1271 }
1272
1273 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (async compatibility)**
1274 ///
1275 /// *For any* error, it should be compatible with async/await patterns,
1276 /// meaning it can be returned from async functions.
1277 ///
1278 /// **Validates: Requirements 5.4**
1279 #[test]
1280 fn prop_error_async_compatible(error in arb_error()) {
1281 // Verify error implements required traits for async usage
1282 fn assert_async_compatible<T: Send + Sync + 'static + std::error::Error>(_: &T) {}
1283 assert_async_compatible(&error);
1284
1285 // Verify error can be boxed as dyn Error (required for anyhow compatibility)
1286 let boxed: Box<dyn std::error::Error + Send + Sync + 'static> = Box::new(error);
1287
1288 // Verify the boxed error can be sent across threads (simulating async task spawn)
1289 let handle = thread::spawn(move || {
1290 boxed.to_string()
1291 });
1292 let _ = handle.join().expect("Thread should not panic");
1293 }
1294 }
1295}