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}