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}