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 using
138//! efficient bitflags storage (8 bytes instead of 46+ bytes for individual booleans):
139//!
140//! ```rust
141//! use ccxt_core::exchange::ExchangeCapabilities;
142//!
143//! // Create capabilities for public-only access
144//! let public_caps = ExchangeCapabilities::public_only();
145//! assert!(public_caps.fetch_ticker());
146//! assert!(!public_caps.create_order());
147//!
148//! // Create capabilities with all features
149//! let all_caps = ExchangeCapabilities::all();
150//! assert!(all_caps.create_order());
151//! assert!(all_caps.websocket());
152//!
153//! // Check capability by name (CCXT-style camelCase)
154//! assert!(all_caps.has("fetchTicker"));
155//! assert!(all_caps.has("createOrder"));
156//!
157//! // Use builder pattern for custom configurations
158//! use ccxt_core::ExchangeCapabilitiesBuilder;
159//! let custom = ExchangeCapabilitiesBuilder::new()
160//! .market_data() // Add all market data capabilities
161//! .trading() // Add all trading capabilities
162//! .build();
163//! ```
164//!
165//! ## Error Handling
166//!
167//! All exchange methods return `Result<T>` with comprehensive error types:
168//!
169//! - `NotImplemented`: Method not supported by this exchange
170//! - `Authentication`: API credentials missing or invalid
171//! - `RateLimit`: Too many requests
172//! - `Network`: Connection or timeout errors
173//! - `Exchange`: Exchange-specific errors
174//!
175//! ## Thread Safety
176//!
177//! The `Exchange` trait requires `Send + Sync` bounds, ensuring:
178//!
179//! - Exchanges can be sent across thread boundaries (`Send`)
180//! - Exchanges can be shared across threads via `Arc` (`Sync`)
181//! - Compatible with Tokio and other async runtimes
182//!
183//! ## See Also
184//!
185//! - [`crate::ws_exchange::WsExchange`]: WebSocket streaming trait
186//! - [`crate::ws_exchange::FullExchange`]: Combined REST + WebSocket trait
187//! - [`crate::base_exchange::BaseExchange`]: Base implementation utilities
188
189use async_trait::async_trait;
190use std::collections::HashMap;
191use std::sync::Arc;
192
193use crate::error::Result;
194use crate::types::financial::{Amount, Price};
195// Lint: wildcard_imports
196// Reason: Types module contains many related types used throughout this file;
197// explicit imports would be excessively verbose and harder to maintain
198#[allow(clippy::wildcard_imports)]
199use crate::types::*;
200
201// Re-export ExchangeCapabilities and related types from the capability module
202// The new implementation uses bitflags for efficient storage (8 bytes instead of 46+ bytes)
203pub use crate::capability::{
204 Capabilities, Capability, ExchangeCapabilities, ExchangeCapabilitiesBuilder,
205};
206
207// Re-export sub-traits for convenience
208// These modular traits allow exchanges to implement only the capabilities they support
209pub use crate::traits::{
210 Account, ArcAccount, ArcFullExchange, ArcFunding, ArcMargin, ArcMarketData, ArcTrading,
211 BoxedAccount, BoxedFullExchange, BoxedFunding, BoxedMargin, BoxedMarketData, BoxedTrading,
212 FullExchange as ModularFullExchange, Funding, Margin, MarketData, PublicExchange, Trading,
213};
214
215// ============================================================================
216// Exchange Trait
217// ============================================================================
218
219/// Core Exchange trait - the unified interface for all exchanges
220///
221/// This trait defines the standard API that all exchange implementations
222/// must provide. It is designed to be object-safe for dynamic dispatch,
223/// allowing exchanges to be used polymorphically via `dyn Exchange`.
224///
225/// # Relationship to Modular Traits
226///
227/// The `Exchange` trait provides a unified interface that combines functionality
228/// from the modular trait hierarchy:
229///
230/// - [`PublicExchange`]: Metadata and capabilities (id, name, capabilities, etc.)
231/// - [`MarketData`]: Public market data (fetch_markets, fetch_ticker, etc.)
232/// - [`Trading`]: Order management (create_order, cancel_order, etc.)
233/// - [`Account`]: Account operations (fetch_balance, fetch_my_trades)
234/// - [`Margin`]: Margin/futures operations (available via separate trait)
235/// - [`Funding`]: Deposit/withdrawal operations (available via separate trait)
236///
237/// For new implementations, consider implementing the modular traits instead,
238/// which allows for more granular capability composition. Types implementing
239/// all modular traits automatically satisfy the requirements for `Exchange`.
240///
241/// # Thread Safety
242///
243/// All implementations must be `Send + Sync` to allow safe usage across
244/// thread boundaries.
245///
246/// # Backward Compatibility
247///
248/// This trait maintains full backward compatibility with existing code.
249/// All methods from the original monolithic trait are still available.
250/// Existing implementations continue to work without modification.
251///
252/// # Example
253///
254/// ```rust,no_run
255/// use ccxt_core::exchange::Exchange;
256///
257/// async fn print_exchange_info(exchange: &dyn Exchange) {
258/// println!("Exchange: {} ({})", exchange.name(), exchange.id());
259/// println!("Version: {}", exchange.version());
260/// println!("Certified: {}", exchange.certified());
261/// }
262/// ```
263#[async_trait]
264pub trait Exchange: Send + Sync {
265 // ==================== Metadata ====================
266
267 /// Returns the exchange identifier (e.g., "binance", "coinbase")
268 ///
269 /// This is a lowercase, URL-safe identifier used internally.
270 fn id(&self) -> &str;
271
272 /// Returns the human-readable exchange name (e.g., "Binance", "Coinbase")
273 fn name(&self) -> &str;
274
275 /// Returns the API version string
276 fn version(&self) -> &'static str {
277 "1.0.0"
278 }
279
280 /// Returns whether this exchange is CCXT certified
281 ///
282 /// Certified exchanges have been thoroughly tested and verified.
283 fn certified(&self) -> bool {
284 false
285 }
286
287 /// Returns whether this exchange supports WebSocket (pro features)
288 fn has_websocket(&self) -> bool {
289 self.capabilities().websocket()
290 }
291
292 /// Returns the exchange capabilities
293 ///
294 /// Use this to check which features are supported before calling methods.
295 fn capabilities(&self) -> ExchangeCapabilities;
296
297 /// Returns supported timeframes for OHLCV data
298 fn timeframes(&self) -> Vec<Timeframe> {
299 vec![
300 Timeframe::M1,
301 Timeframe::M5,
302 Timeframe::M15,
303 Timeframe::H1,
304 Timeframe::H4,
305 Timeframe::D1,
306 ]
307 }
308
309 /// Returns the rate limit (requests per second)
310 fn rate_limit(&self) -> u32 {
311 10
312 }
313
314 // ==================== Market Data (Public API) ====================
315
316 /// Fetch all available markets
317 ///
318 /// # Returns
319 ///
320 /// A vector of `Market` structs containing market definitions.
321 ///
322 /// # Errors
323 ///
324 /// Returns an error if the request fails or the exchange is unavailable.
325 async fn fetch_markets(&self) -> Result<Vec<Market>>;
326
327 /// Load markets and cache them
328 ///
329 /// # Arguments
330 ///
331 /// * `reload` - If true, force reload even if markets are cached
332 ///
333 /// # Returns
334 ///
335 /// A HashMap of markets indexed by symbol.
336 async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>>;
337
338 /// Fetch ticker for a single symbol
339 ///
340 /// # Arguments
341 ///
342 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
343 ///
344 /// # Returns
345 ///
346 /// The ticker data for the specified symbol.
347 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker>;
348
349 /// Fetch tickers for multiple symbols (or all if None)
350 ///
351 /// # Arguments
352 ///
353 /// * `symbols` - Optional list of symbols to fetch. If None, fetches all.
354 ///
355 /// # Returns
356 ///
357 /// A vector of tickers.
358 async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>>;
359
360 /// Fetch order book for a symbol
361 ///
362 /// # Arguments
363 ///
364 /// * `symbol` - Trading pair symbol
365 /// * `limit` - Optional limit on the number of orders per side
366 ///
367 /// # Returns
368 ///
369 /// The order book containing bids and asks.
370 async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook>;
371
372 /// Fetch recent public trades
373 ///
374 /// # Arguments
375 ///
376 /// * `symbol` - Trading pair symbol
377 /// * `limit` - Optional limit on the number of trades
378 ///
379 /// # Returns
380 ///
381 /// A vector of recent trades.
382 async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>>;
383
384 /// Fetch OHLCV candlestick data
385 ///
386 /// # Arguments
387 ///
388 /// * `symbol` - Trading pair symbol
389 /// * `timeframe` - Candlestick timeframe
390 /// * `since` - Optional start timestamp in milliseconds
391 /// * `limit` - Optional limit on the number of candles
392 ///
393 /// # Returns
394 ///
395 /// A vector of OHLCV candles.
396 async fn fetch_ohlcv(
397 &self,
398 symbol: &str,
399 timeframe: Timeframe,
400 since: Option<i64>,
401 limit: Option<u32>,
402 ) -> Result<Vec<Ohlcv>>;
403
404 // ==================== Trading (Private API) ====================
405
406 /// Create a new order
407 ///
408 /// # Arguments
409 ///
410 /// * `symbol` - Trading pair symbol
411 /// * `order_type` - Order type (limit, market, etc.)
412 /// * `side` - Order side (buy or sell)
413 /// * `amount` - Order amount (type-safe Amount wrapper)
414 /// * `price` - Optional price (required for limit orders, type-safe Price wrapper)
415 ///
416 /// # Returns
417 ///
418 /// The created order.
419 ///
420 /// # Errors
421 ///
422 /// Returns an error if authentication fails or the order is invalid.
423 async fn create_order(
424 &self,
425 symbol: &str,
426 order_type: OrderType,
427 side: OrderSide,
428 amount: Amount,
429 price: Option<Price>,
430 ) -> Result<Order>;
431
432 /// Cancel an existing order
433 ///
434 /// # Arguments
435 ///
436 /// * `id` - Order ID to cancel
437 /// * `symbol` - Optional symbol (required by some exchanges)
438 ///
439 /// # Returns
440 ///
441 /// The canceled order.
442 async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order>;
443
444 /// Cancel all orders (optionally for a specific symbol)
445 ///
446 /// # Arguments
447 ///
448 /// * `symbol` - Optional symbol to cancel orders for
449 ///
450 /// # Returns
451 ///
452 /// A vector of canceled orders.
453 async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>>;
454
455 /// Fetch a specific order by ID
456 ///
457 /// # Arguments
458 ///
459 /// * `id` - Order ID
460 /// * `symbol` - Optional symbol (required by some exchanges)
461 ///
462 /// # Returns
463 ///
464 /// The order details.
465 async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order>;
466
467 /// Fetch all open orders
468 ///
469 /// # Arguments
470 ///
471 /// * `symbol` - Optional symbol to filter by
472 /// * `since` - Optional start timestamp
473 /// * `limit` - Optional limit on results
474 ///
475 /// # Returns
476 ///
477 /// A vector of open orders.
478 async fn fetch_open_orders(
479 &self,
480 symbol: Option<&str>,
481 since: Option<i64>,
482 limit: Option<u32>,
483 ) -> Result<Vec<Order>>;
484
485 /// Fetch closed orders
486 ///
487 /// # Arguments
488 ///
489 /// * `symbol` - Optional symbol to filter by
490 /// * `since` - Optional start timestamp
491 /// * `limit` - Optional limit on results
492 ///
493 /// # Returns
494 ///
495 /// A vector of closed orders.
496 async fn fetch_closed_orders(
497 &self,
498 symbol: Option<&str>,
499 since: Option<i64>,
500 limit: Option<u32>,
501 ) -> Result<Vec<Order>>;
502
503 // ==================== Account (Private API) ====================
504
505 /// Fetch account balance
506 ///
507 /// # Returns
508 ///
509 /// The account balance containing all currencies.
510 async fn fetch_balance(&self) -> Result<Balance>;
511
512 /// Fetch user's trade history
513 ///
514 /// # Arguments
515 ///
516 /// * `symbol` - Optional symbol to filter by
517 /// * `since` - Optional start timestamp
518 /// * `limit` - Optional limit on results
519 ///
520 /// # Returns
521 ///
522 /// A vector of user's trades.
523 async fn fetch_my_trades(
524 &self,
525 symbol: Option<&str>,
526 since: Option<i64>,
527 limit: Option<u32>,
528 ) -> Result<Vec<Trade>>;
529
530 // ==================== Helper Methods ====================
531
532 /// Get a specific market by symbol
533 ///
534 /// # Arguments
535 ///
536 /// * `symbol` - Trading pair symbol
537 ///
538 /// # Returns
539 ///
540 /// The market definition.
541 ///
542 /// # Errors
543 ///
544 /// Returns an error if the market is not found or markets are not loaded.
545 async fn market(&self, symbol: &str) -> Result<Arc<Market>>;
546
547 /// Get all cached markets
548 ///
549 /// # Returns
550 ///
551 /// A HashMap of all markets indexed by symbol.
552 async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>>;
553
554 /// Check if a symbol is valid and active
555 ///
556 /// # Arguments
557 ///
558 /// * `symbol` - Trading pair symbol
559 ///
560 /// # Returns
561 ///
562 /// True if the symbol exists and is active.
563 async fn is_symbol_active(&self, symbol: &str) -> bool {
564 self.market(symbol).await.map(|m| m.active).unwrap_or(false)
565 }
566}
567
568// ============================================================================
569// Type Aliases
570// ============================================================================
571
572/// Type alias for a boxed Exchange trait object
573///
574/// Use this when you need owned, heap-allocated exchange instances.
575pub type BoxedExchange = Box<dyn Exchange>;
576
577/// Type alias for an Arc-wrapped Exchange trait object
578///
579/// Use this when you need shared ownership across threads.
580pub type ArcExchange = Arc<dyn Exchange>;
581
582// ============================================================================
583// Exchange Extension Trait
584// ============================================================================
585
586/// Extension trait providing access to modular sub-traits from Exchange.
587///
588/// This trait provides helper methods to access the modular trait interfaces
589/// from an Exchange implementation. It enables gradual migration from the
590/// monolithic Exchange trait to the modular trait hierarchy.
591///
592/// # Example
593///
594/// ```rust,ignore
595/// use ccxt_core::exchange::{Exchange, ExchangeExt};
596///
597/// async fn use_modular_traits(exchange: &dyn Exchange) {
598/// // Access market data functionality
599/// if let Some(market_data) = exchange.as_market_data() {
600/// let ticker = market_data.fetch_ticker("BTC/USDT").await;
601/// }
602/// }
603/// ```
604pub trait ExchangeExt: Exchange {
605 /// Check if this exchange implements the MarketData trait.
606 ///
607 /// Returns true if the exchange supports market data operations.
608 fn supports_market_data(&self) -> bool {
609 self.capabilities().fetch_markets() || self.capabilities().fetch_ticker()
610 }
611
612 /// Check if this exchange implements the Trading trait.
613 ///
614 /// Returns true if the exchange supports trading operations.
615 fn supports_trading(&self) -> bool {
616 self.capabilities().create_order()
617 }
618
619 /// Check if this exchange implements the Account trait.
620 ///
621 /// Returns true if the exchange supports account operations.
622 fn supports_account(&self) -> bool {
623 self.capabilities().fetch_balance()
624 }
625
626 /// Check if this exchange implements the Margin trait.
627 ///
628 /// Returns true if the exchange supports margin/futures operations.
629 fn supports_margin(&self) -> bool {
630 self.capabilities().fetch_positions()
631 }
632
633 /// Check if this exchange implements the Funding trait.
634 ///
635 /// Returns true if the exchange supports funding operations.
636 fn supports_funding(&self) -> bool {
637 self.capabilities().withdraw()
638 }
639}
640
641/// Blanket implementation of ExchangeExt for all Exchange implementations.
642impl<T: Exchange + ?Sized> ExchangeExt for T {}
643
644// ============================================================================
645// Tests
646// ============================================================================
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn test_capabilities_default() {
654 let caps = ExchangeCapabilities::default();
655 assert!(!caps.fetch_ticker());
656 assert!(!caps.create_order());
657 assert!(!caps.websocket());
658 }
659
660 #[test]
661 fn test_capabilities_all() {
662 let caps = ExchangeCapabilities::all();
663 assert!(caps.fetch_ticker());
664 assert!(caps.create_order());
665 assert!(caps.websocket());
666 assert!(caps.fetch_ohlcv());
667 assert!(caps.fetch_balance());
668 }
669
670 #[test]
671 fn test_capabilities_public_only() {
672 let caps = ExchangeCapabilities::public_only();
673 assert!(caps.fetch_ticker());
674 assert!(caps.fetch_order_book());
675 assert!(caps.fetch_trades());
676 assert!(!caps.create_order());
677 assert!(!caps.fetch_balance());
678 assert!(!caps.websocket());
679 }
680
681 #[test]
682 fn test_capabilities_has() {
683 let caps = ExchangeCapabilities::all();
684 assert!(caps.has("fetchTicker"));
685 assert!(caps.has("createOrder"));
686 assert!(caps.has("websocket"));
687 assert!(!caps.has("unknownCapability"));
688 }
689
690 #[test]
691 fn test_capabilities_supported_list() {
692 let caps = ExchangeCapabilities::public_only();
693 let supported = caps.supported_capabilities();
694 assert!(supported.contains(&"fetchTicker"));
695 assert!(supported.contains(&"fetchOrderBook"));
696 assert!(!supported.contains(&"createOrder"));
697 }
698
699 #[test]
700 fn test_capabilities_equality() {
701 let caps1 = ExchangeCapabilities::all();
702 let caps2 = ExchangeCapabilities::all();
703 assert_eq!(caps1, caps2);
704
705 let caps3 = ExchangeCapabilities::public_only();
706 assert_ne!(caps1, caps3);
707 }
708}
709
710#[cfg(test)]
711mod property_tests {
712 use super::*;
713 use crate::error::Error;
714 use proptest::prelude::*;
715 use std::thread;
716
717 // ==================== Strategies ====================
718
719 /// Strategy to generate arbitrary ExchangeCapabilities using builder API
720 fn arb_capabilities() -> impl Strategy<Value = ExchangeCapabilities> {
721 prop_oneof![
722 Just(ExchangeCapabilities::default()),
723 Just(ExchangeCapabilities::all()),
724 Just(ExchangeCapabilities::public_only()),
725 // Random capabilities using builder
726 (
727 prop::bool::ANY,
728 prop::bool::ANY,
729 prop::bool::ANY,
730 prop::bool::ANY,
731 prop::bool::ANY,
732 prop::bool::ANY,
733 )
734 .prop_map(
735 |(
736 fetch_ticker,
737 fetch_order_book,
738 create_order,
739 websocket,
740 fetch_balance,
741 fetch_ohlcv,
742 )| {
743 let mut builder = ExchangeCapabilities::builder();
744 if fetch_ticker {
745 builder = builder.capability(Capability::FetchTicker);
746 }
747 if fetch_order_book {
748 builder = builder.capability(Capability::FetchOrderBook);
749 }
750 if create_order {
751 builder = builder.capability(Capability::CreateOrder);
752 }
753 if websocket {
754 builder = builder.capability(Capability::Websocket);
755 }
756 if fetch_balance {
757 builder = builder.capability(Capability::FetchBalance);
758 }
759 if fetch_ohlcv {
760 builder = builder.capability(Capability::FetchOhlcv);
761 }
762 builder.build()
763 }
764 ),
765 ]
766 }
767
768 /// Strategy to generate arbitrary error messages
769 fn arb_error_message() -> impl Strategy<Value = String> {
770 prop_oneof![
771 Just("".to_string()),
772 "[a-zA-Z0-9 .,!?-]{1,100}",
773 // Unicode messages
774 "\\PC{1,50}",
775 ]
776 }
777
778 /// Strategy to generate arbitrary Error variants for testing error propagation
779 fn arb_error() -> impl Strategy<Value = Error> {
780 prop_oneof![
781 // Authentication errors
782 arb_error_message().prop_map(|msg| Error::authentication(msg)),
783 // Invalid request errors
784 arb_error_message().prop_map(|msg| Error::invalid_request(msg)),
785 // Market not found errors
786 arb_error_message().prop_map(|msg| Error::market_not_found(msg)),
787 // Timeout errors
788 arb_error_message().prop_map(|msg| Error::timeout(msg)),
789 // Not implemented errors
790 arb_error_message().prop_map(|msg| Error::not_implemented(msg)),
791 // Network errors
792 arb_error_message().prop_map(|msg| Error::network(msg)),
793 // WebSocket errors
794 arb_error_message().prop_map(|msg| Error::websocket(msg)),
795 ]
796 }
797
798 // ==================== Property 3: Thread Safety ====================
799
800 proptest! {
801 #![proptest_config(ProptestConfig::with_cases(100))]
802
803 /// **Feature: unified-exchange-trait, Property 3: Thread Safety**
804 ///
805 /// *For any* exchange trait object, it should be possible to send it across
806 /// thread boundaries (`Send`) and share references across threads (`Sync`).
807 #[test]
808 fn prop_exchange_capabilities_send_sync(caps in arb_capabilities()) {
809 // Compile-time assertion: ExchangeCapabilities must be Send + Sync
810 fn assert_send_sync<T: Send + Sync>(_: &T) {}
811 assert_send_sync(&caps);
812
813 // Runtime verification: ExchangeCapabilities can be sent across threads
814 let caps_clone = caps.clone();
815 let handle = thread::spawn(move || {
816 // Capabilities were successfully moved to another thread (Send)
817 caps_clone.fetch_ticker()
818 });
819 let result = handle.join().expect("Thread should not panic");
820 prop_assert_eq!(result, caps.fetch_ticker());
821 }
822
823 #[test]
824 fn prop_exchange_capabilities_arc_sharing(caps in arb_capabilities()) {
825 use std::sync::Arc;
826
827 let shared_caps = Arc::new(caps.clone());
828
829 // Spawn multiple threads that read from the shared capabilities
830 let handles: Vec<_> = (0..4)
831 .map(|_| {
832 let caps_ref = Arc::clone(&shared_caps);
833 thread::spawn(move || {
834 // Read various capabilities from different threads
835 (
836 caps_ref.fetch_ticker(),
837 caps_ref.create_order(),
838 caps_ref.websocket(),
839 )
840 })
841 })
842 .collect();
843
844 // All threads should complete successfully with consistent values
845 for handle in handles {
846 let (fetch_ticker, create_order, websocket) =
847 handle.join().expect("Thread should not panic");
848 prop_assert_eq!(fetch_ticker, caps.fetch_ticker());
849 prop_assert_eq!(create_order, caps.create_order());
850 prop_assert_eq!(websocket, caps.websocket());
851 }
852 }
853
854 /// **Feature: unified-exchange-trait, Property 3: Thread Safety (BoxedExchange type alias)**
855 ///
856 /// Verifies that the BoxedExchange type alias (Box<dyn Exchange>) satisfies
857 /// Send + Sync bounds required for async runtime usage.
858 #[test]
859 fn prop_boxed_exchange_type_is_send_sync(_dummy in Just(())) {
860 // Compile-time assertion: BoxedExchange must be Send
861 fn assert_send<T: Send>() {}
862 assert_send::<BoxedExchange>();
863
864 // Note: Box<dyn Exchange> is Send because Exchange: Send + Sync
865 // This is a compile-time check that validates the trait bounds
866 prop_assert!(true, "BoxedExchange type satisfies Send bound");
867 }
868
869 /// **Feature: unified-exchange-trait, Property 3: Thread Safety (ArcExchange type alias)**
870 ///
871 /// Verifies that the ArcExchange type alias (Arc<dyn Exchange>) satisfies
872 /// Send + Sync bounds required for shared ownership across threads.
873 #[test]
874 fn prop_arc_exchange_type_is_send_sync(_dummy in Just(())) {
875 // Compile-time assertion: ArcExchange must be Send + Sync
876 fn assert_send_sync<T: Send + Sync>() {}
877 assert_send_sync::<ArcExchange>();
878
879 // This is a compile-time check that validates the trait bounds
880 prop_assert!(true, "ArcExchange type satisfies Send + Sync bounds");
881 }
882 }
883
884 // ==================== Property 4: Error Propagation ====================
885
886 proptest! {
887 #![proptest_config(ProptestConfig::with_cases(100))]
888
889 /// **Feature: unified-exchange-trait, Property 4: Error Propagation**
890 ///
891 /// *For any* async method call that fails, the error should be properly
892 /// propagated through the `Result` type without panicking.
893 #[test]
894 fn prop_error_propagation_through_result(error in arb_error()) {
895 // Store the error string before moving
896 let error_string = error.to_string();
897
898 // Create a Result with the error
899 let result: Result<()> = Err(error);
900
901 // Verify error can be extracted without panicking
902 prop_assert!(result.is_err());
903
904 let extracted_error = result.unwrap_err();
905
906 // Error should preserve its display message
907 prop_assert_eq!(
908 extracted_error.to_string(),
909 error_string,
910 "Error display should be preserved"
911 );
912 }
913
914 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (with context)**
915 ///
916 /// *For any* error with context attached, the context should be preserved
917 /// and the error chain should be traversable.
918 #[test]
919 fn prop_error_propagation_with_context(
920 base_error in arb_error(),
921 context in "[a-zA-Z0-9 ]{1,50}"
922 ) {
923 // Add context to the error
924 let error_with_context = base_error.context(context.clone());
925
926 // The error display should contain the context
927 let display = error_with_context.to_string();
928 prop_assert!(
929 display.contains(&context),
930 "Error display '{}' should contain context '{}'",
931 display,
932 context
933 );
934
935 // Error should still be usable in Result
936 let result: Result<()> = Err(error_with_context);
937 prop_assert!(result.is_err());
938 }
939
940 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (Send + Sync)**
941 ///
942 /// *For any* error, it should be possible to send it across thread boundaries,
943 /// which is essential for async error propagation.
944 #[test]
945 fn prop_error_send_across_threads(error in arb_error()) {
946 // Compile-time assertion: Error must be Send + Sync
947 fn assert_send_sync<T: Send + Sync + 'static>(_: &T) {}
948 assert_send_sync(&error);
949
950 // Runtime verification: Error can be sent across threads
951 let error_string = error.to_string();
952 let handle = thread::spawn(move || {
953 // Error was successfully moved to another thread (Send)
954 error.to_string()
955 });
956 let result = handle.join().expect("Thread should not panic");
957 prop_assert_eq!(result, error_string);
958 }
959
960 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (Result chain)**
961 ///
962 /// *For any* sequence of operations that may fail, errors should propagate
963 /// correctly through the ? operator pattern.
964 #[test]
965 fn prop_error_propagation_chain(
966 error_msg in arb_error_message(),
967 should_fail_first in prop::bool::ANY,
968 should_fail_second in prop::bool::ANY
969 ) {
970 fn operation_one(fail: bool, msg: &str) -> Result<i32> {
971 if fail {
972 Err(Error::invalid_request(msg.to_string()))
973 } else {
974 Ok(42)
975 }
976 }
977
978 fn operation_two(fail: bool, msg: &str, input: i32) -> Result<i32> {
979 if fail {
980 Err(Error::invalid_request(msg.to_string()))
981 } else {
982 Ok(input * 2)
983 }
984 }
985
986 fn chained_operations(
987 fail_first: bool,
988 fail_second: bool,
989 msg: &str,
990 ) -> Result<i32> {
991 let result = operation_one(fail_first, msg)?;
992 operation_two(fail_second, msg, result)
993 }
994
995 let result = chained_operations(should_fail_first, should_fail_second, &error_msg);
996
997 // Verify the result matches expected behavior
998 if should_fail_first {
999 prop_assert!(result.is_err(), "Should fail on first operation");
1000 } else if should_fail_second {
1001 prop_assert!(result.is_err(), "Should fail on second operation");
1002 } else {
1003 prop_assert!(result.is_ok(), "Should succeed when no failures");
1004 prop_assert_eq!(result.unwrap(), 84, "Result should be 42 * 2 = 84");
1005 }
1006 }
1007
1008 /// **Feature: unified-exchange-trait, Property 4: Error Propagation (async compatibility)**
1009 ///
1010 /// *For any* error, it should be compatible with async/await patterns,
1011 /// meaning it can be returned from async functions.
1012 #[test]
1013 fn prop_error_async_compatible(error in arb_error()) {
1014 // Verify error implements required traits for async usage
1015 fn assert_async_compatible<T: Send + Sync + 'static + std::error::Error>(_: &T) {}
1016 assert_async_compatible(&error);
1017
1018 // Verify error can be boxed as dyn Error (required for anyhow compatibility)
1019 let boxed: Box<dyn std::error::Error + Send + Sync + 'static> = Box::new(error);
1020
1021 // Verify the boxed error can be sent across threads (simulating async task spawn)
1022 let handle = thread::spawn(move || {
1023 boxed.to_string()
1024 });
1025 let _ = handle.join().expect("Thread should not panic");
1026 }
1027 }
1028}