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