Skip to main content

wickra_exchange_core/
options.rs

1//! Per-connection configuration.
2//!
3//! [`MarketType`] is the largest structural axis: a single venue such as Binance
4//! is really three or four APIs — spot, USDⓈ-M futures, COIN-M futures, margin —
5//! with different base URLs, endpoints and symbol filters. [`ExchangeOptions`]
6//! carries that choice plus the transport knobs (testnet, receive window,
7//! timeout, proxy) and the perpetual-specific position/margin modes.
8
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12/// Which market of a venue to connect to.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum MarketType {
15    /// Spot market.
16    Spot,
17    /// USDⓈ-margined linear perpetual / futures.
18    UsdMFutures,
19    /// Coin-margined inverse perpetual / futures.
20    CoinMFutures,
21    /// Cross/isolated margin spot.
22    Margin,
23}
24
25impl MarketType {
26    /// Whether this market is a derivatives (futures/perp) market, where
27    /// leverage, funding and position mode apply.
28    #[must_use]
29    pub fn is_derivatives(self) -> bool {
30        matches!(self, MarketType::UsdMFutures | MarketType::CoinMFutures)
31    }
32
33    /// This market in words, for the message a client refuses with.
34    #[must_use]
35    pub fn describe(self) -> &'static str {
36        match self {
37            MarketType::Spot => "the spot market",
38            MarketType::UsdMFutures => "USD-margined (linear) futures",
39            MarketType::CoinMFutures => "coin-margined (inverse) futures",
40            MarketType::Margin => "the margin market",
41        }
42    }
43}
44
45/// Position mode for derivatives accounts.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum PositionMode {
48    /// A single net position per symbol.
49    OneWay,
50    /// Separate long and short positions per symbol.
51    Hedge,
52}
53
54/// Margin mode for derivatives accounts.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub enum MarginMode {
57    /// Margin shared across positions.
58    Cross,
59    /// Margin isolated per position.
60    Isolated,
61}
62
63/// Self-trade-prevention (STP) policy: what a venue does when an order would
64/// match against another order from the same account.
65#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum SelfTradePrevention {
67    /// No self-trade prevention (the venue default).
68    #[default]
69    None,
70    /// Expire the resting (maker) order in the self-match.
71    ExpireMaker,
72    /// Expire the incoming (taker) order in the self-match.
73    ExpireTaker,
74    /// Expire both orders in the self-match.
75    ExpireBoth,
76}
77
78/// Configuration for one exchange connection.
79#[derive(Debug, Clone)]
80pub struct ExchangeOptions {
81    /// Which market of the venue to use.
82    pub market_type: MarketType,
83    /// Use the venue's testnet/sandbox rather than mainnet.
84    pub testnet: bool,
85    /// The signed-request receive window in milliseconds (how long a signed
86    /// request stays valid against the server clock).
87    pub recv_window_ms: u64,
88    /// Per-request timeout.
89    pub timeout: Duration,
90    /// Optional `User-Agent` header override.
91    pub user_agent: Option<String>,
92    /// Optional HTTP/HTTPS proxy URL.
93    pub proxy: Option<String>,
94    /// Derivatives position mode.
95    pub position_mode: PositionMode,
96    /// Derivatives margin mode.
97    pub margin_mode: MarginMode,
98}
99
100impl ExchangeOptions {
101    /// Mainnet options for the given market, with sensible defaults.
102    #[must_use]
103    pub fn mainnet(market_type: MarketType) -> Self {
104        Self {
105            market_type,
106            testnet: false,
107            recv_window_ms: 5_000,
108            timeout: Duration::from_secs(10),
109            user_agent: None,
110            proxy: None,
111            position_mode: PositionMode::OneWay,
112            margin_mode: MarginMode::Cross,
113        }
114    }
115
116    /// Testnet/sandbox options for the given market.
117    #[must_use]
118    pub fn testnet(market_type: MarketType) -> Self {
119        Self {
120            testnet: true,
121            ..Self::mainnet(market_type)
122        }
123    }
124
125    /// Override the receive window (milliseconds).
126    #[must_use]
127    pub fn with_recv_window(mut self, ms: u64) -> Self {
128        self.recv_window_ms = ms;
129        self
130    }
131
132    /// Override the per-request timeout.
133    #[must_use]
134    pub fn with_timeout(mut self, timeout: Duration) -> Self {
135        self.timeout = timeout;
136        self
137    }
138
139    /// Override the position mode (derivatives).
140    #[must_use]
141    pub fn with_position_mode(mut self, mode: PositionMode) -> Self {
142        self.position_mode = mode;
143        self
144    }
145
146    /// Override the margin mode (derivatives).
147    #[must_use]
148    pub fn with_margin_mode(mut self, mode: MarginMode) -> Self {
149        self.margin_mode = mode;
150        self
151    }
152}
153
154impl Default for ExchangeOptions {
155    /// Mainnet spot with default transport knobs.
156    fn default() -> Self {
157        Self::mainnet(MarketType::Spot)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn derivatives_classification() {
167        assert!(MarketType::UsdMFutures.is_derivatives());
168        assert!(MarketType::CoinMFutures.is_derivatives());
169        assert!(!MarketType::Spot.is_derivatives());
170        assert!(!MarketType::Margin.is_derivatives());
171    }
172
173    #[test]
174    fn testnet_flips_only_the_flag() {
175        let main = ExchangeOptions::mainnet(MarketType::Spot);
176        let test = ExchangeOptions::testnet(MarketType::Spot);
177        assert!(!main.testnet);
178        assert!(test.testnet);
179        assert_eq!(main.recv_window_ms, test.recv_window_ms);
180        assert_eq!(main.timeout, test.timeout);
181    }
182
183    #[test]
184    fn builders_override_fields() {
185        let opts = ExchangeOptions::mainnet(MarketType::UsdMFutures)
186            .with_recv_window(10_000)
187            .with_timeout(Duration::from_secs(30))
188            .with_position_mode(PositionMode::Hedge)
189            .with_margin_mode(MarginMode::Isolated);
190        assert_eq!(opts.recv_window_ms, 10_000);
191        assert_eq!(opts.timeout, Duration::from_secs(30));
192        assert_eq!(opts.position_mode, PositionMode::Hedge);
193        assert_eq!(opts.margin_mode, MarginMode::Isolated);
194    }
195
196    #[test]
197    fn default_is_mainnet_spot() {
198        let opts = ExchangeOptions::default();
199        assert_eq!(opts.market_type, MarketType::Spot);
200        assert!(!opts.testnet);
201    }
202}