Skip to main content

nautilus_coinbase/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration structures for the Coinbase adapter.
17
18#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use nautilus_model::{enums::AccountType, identifiers::AccountId};
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{
26    enums::{CoinbaseEnvironment, CoinbaseMarginType},
27    urls,
28};
29
30/// Configuration for the Coinbase data client.
31#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
32#[serde(default, deny_unknown_fields)]
33#[cfg_attr(
34    feature = "python",
35    pyo3::pyclass(module = "nautilus_trader.adapters.coinbase", from_py_object)
36)]
37#[cfg_attr(
38    feature = "python",
39    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinbase")
40)]
41pub struct CoinbaseDataClientConfig {
42    /// CDP API key name (falls back to `COINBASE_API_KEY` env var).
43    pub api_key: Option<SecretString>,
44    /// CDP API secret in PEM format (falls back to `COINBASE_API_SECRET` env var).
45    pub api_secret: Option<SecretString>,
46    /// The Coinbase environment to connect to.
47    #[builder(default)]
48    pub environment: CoinbaseEnvironment,
49    /// Override for the REST API base URL.
50    pub base_url_rest: Option<String>,
51    /// Override for the WebSocket market data URL.
52    pub base_url_ws: Option<String>,
53    /// Optional proxy URL for HTTP and WebSocket transports.
54    pub proxy_url: Option<SecretString>,
55    /// HTTP timeout in seconds.
56    #[builder(default = 10)]
57    pub http_timeout_secs: u64,
58    /// WebSocket timeout in seconds.
59    #[builder(default = 30)]
60    pub ws_timeout_secs: u64,
61    /// Interval for refreshing instruments in minutes.
62    #[builder(default = 60)]
63    pub update_instruments_interval_mins: u64,
64    /// Seconds between REST polls for derivatives-only data streams
65    /// (`IndexPriceUpdate`, `FundingRateUpdate`). Coinbase Advanced Trade
66    /// does not publish these on a WebSocket channel, so they are sourced
67    /// from periodic `/products/{id}` fetches.
68    #[builder(default = 15)]
69    pub derivatives_poll_interval_secs: u64,
70    /// WebSocket transport backend (defaults to `Tungstenite`).
71    #[builder(default)]
72    pub transport_backend: TransportBackend,
73}
74
75#[cfg(feature = "python")]
76nautilus_core::impl_pyo3_config_getters!(CoinbaseDataClientConfig {
77    environment: CoinbaseEnvironment,
78    base_url_rest: Option<String>,
79    base_url_ws: Option<String>,
80    http_timeout_secs: u64,
81    ws_timeout_secs: u64,
82    update_instruments_interval_mins: u64,
83    derivatives_poll_interval_secs: u64,
84    transport_backend: TransportBackend,
85});
86
87impl Default for CoinbaseDataClientConfig {
88    fn default() -> Self {
89        Self::builder().build()
90    }
91}
92
93impl CoinbaseDataClientConfig {
94    /// Creates a new configuration with default settings.
95    #[must_use]
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Returns true when credentials are populated and non-empty.
101    #[must_use]
102    pub fn has_credentials(&self) -> bool {
103        self.api_key
104            .as_ref()
105            .map(SecretString::expose_secret)
106            .is_some_and(|s| !s.trim().is_empty())
107            && self
108                .api_secret
109                .as_ref()
110                .map(SecretString::expose_secret)
111                .is_some_and(|s| !s.trim().is_empty())
112    }
113
114    /// Returns the REST API base URL, respecting environment and overrides.
115    #[must_use]
116    pub fn rest_url(&self) -> String {
117        self.base_url_rest
118            .clone()
119            .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
120    }
121
122    /// Returns the WebSocket market data URL, respecting environment and overrides.
123    #[must_use]
124    pub fn ws_url(&self) -> String {
125        self.base_url_ws
126            .clone()
127            .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
128    }
129}
130
131/// Configuration for the Coinbase execution client.
132#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
133#[serde(default, deny_unknown_fields)]
134#[cfg_attr(
135    feature = "python",
136    pyo3::pyclass(module = "nautilus_trader.adapters.coinbase", from_py_object)
137)]
138#[cfg_attr(
139    feature = "python",
140    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinbase")
141)]
142pub struct CoinbaseExecutionClientConfig {
143    /// Account identifier for the execution client.
144    #[builder(default = AccountId::from("COINBASE-001"))]
145    pub account_id: AccountId,
146    /// CDP API key name (falls back to `COINBASE_API_KEY` env var).
147    pub api_key: Option<SecretString>,
148    /// CDP API secret in PEM format (falls back to `COINBASE_API_SECRET` env var).
149    pub api_secret: Option<SecretString>,
150    /// Environment to connect to.
151    #[builder(default)]
152    pub environment: CoinbaseEnvironment,
153    /// Optional override for the REST API base URL.
154    pub base_url_rest: Option<String>,
155    /// Optional override for the WebSocket user data URL.
156    pub base_url_ws: Option<String>,
157    /// Optional proxy URL for HTTP and WebSocket transports.
158    pub proxy_url: Option<SecretString>,
159    /// Timeout in seconds for HTTP requests.
160    #[builder(default = 10)]
161    pub http_timeout_secs: u64,
162    /// Maximum number of retry attempts for HTTP requests.
163    #[builder(default = 3)]
164    pub max_retries: u32,
165    /// Initial retry delay in milliseconds.
166    #[builder(default = 100)]
167    pub retry_delay_initial_ms: u64,
168    /// Maximum retry delay in milliseconds.
169    #[builder(default = 5000)]
170    pub retry_delay_max_ms: u64,
171    /// Selects the execution scope: `Cash` for spot, `Margin` for CFM
172    /// derivatives. `CoinbaseExecutionClientFactory` rejects other values.
173    #[builder(default = AccountType::Cash)]
174    pub account_type: AccountType,
175    /// Optional default margin type applied to derivatives orders. Ignored on
176    /// Cash accounts.
177    pub default_margin_type: Option<CoinbaseMarginType>,
178    /// Optional default leverage applied to derivatives orders. Ignored on
179    /// Cash accounts.
180    pub default_leverage: Option<rust_decimal::Decimal>,
181    /// CDP retail portfolio UUID required when the API key is bound to a
182    /// non-default portfolio. When unset, the venue uses the key's default
183    /// portfolio. Coinbase rejects orders with `"account is not available"`
184    /// if the portfolio is non-default and this field is omitted.
185    pub retail_portfolio_id: Option<String>,
186    /// WebSocket transport backend (defaults to `Tungstenite`).
187    #[builder(default)]
188    pub transport_backend: TransportBackend,
189}
190
191#[cfg(feature = "python")]
192nautilus_core::impl_pyo3_config_getters!(CoinbaseExecutionClientConfig {
193    account_id: AccountId,
194    environment: CoinbaseEnvironment,
195    base_url_rest: Option<String>,
196    base_url_ws: Option<String>,
197    http_timeout_secs: u64,
198    max_retries: u32,
199    retry_delay_initial_ms: u64,
200    retry_delay_max_ms: u64,
201    account_type: AccountType,
202    default_margin_type: Option<CoinbaseMarginType>,
203    default_leverage: Option<rust_decimal::Decimal>,
204    retail_portfolio_id: Option<String>,
205    transport_backend: TransportBackend,
206});
207
208impl Default for CoinbaseExecutionClientConfig {
209    fn default() -> Self {
210        Self::builder().build()
211    }
212}
213
214impl CoinbaseExecutionClientConfig {
215    /// Creates a new configuration with default settings.
216    #[must_use]
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    /// Returns true when credentials are populated and non-empty.
222    #[must_use]
223    pub fn has_credentials(&self) -> bool {
224        self.api_key
225            .as_ref()
226            .map(SecretString::expose_secret)
227            .is_some_and(|s| !s.trim().is_empty())
228            && self
229                .api_secret
230                .as_ref()
231                .map(SecretString::expose_secret)
232                .is_some_and(|s| !s.trim().is_empty())
233    }
234
235    /// Returns the REST API base URL, respecting environment and overrides.
236    #[must_use]
237    pub fn rest_url(&self) -> String {
238        self.base_url_rest
239            .clone()
240            .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
241    }
242
243    /// Returns the WebSocket user data URL, respecting environment and overrides.
244    #[must_use]
245    pub fn ws_url(&self) -> String {
246        self.base_url_ws
247            .clone()
248            .unwrap_or_else(|| urls::ws_user_url(self.environment).to_string())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use rstest::rstest;
255
256    use super::*;
257
258    #[rstest]
259    fn test_config_debug_redacts_credentials() {
260        let data = CoinbaseDataClientConfig {
261            api_key: Some("data-api-key".into()),
262            api_secret: Some("data-api-secret".into()),
263            proxy_url: Some("http://user:data-proxy@localhost".into()),
264            ..Default::default()
265        };
266        let execution = CoinbaseExecutionClientConfig {
267            api_key: Some("exec-api-key".into()),
268            api_secret: Some("exec-api-secret".into()),
269            proxy_url: Some("http://user:exec-proxy@localhost".into()),
270            ..Default::default()
271        };
272
273        let formatted = format!("{data:?} {execution:?}");
274
275        assert_eq!(formatted.matches(REDACTED).count(), 6);
276
277        for secret in [
278            "data-api-key",
279            "data-api-secret",
280            "data-proxy",
281            "exec-api-key",
282            "exec-api-secret",
283            "exec-proxy",
284        ] {
285            assert!(!formatted.contains(secret));
286        }
287    }
288
289    #[rstest]
290    fn test_data_config_defaults() {
291        let config = CoinbaseDataClientConfig::default();
292        assert_eq!(config.environment, CoinbaseEnvironment::Live);
293        assert_eq!(config.http_timeout_secs, 10);
294        assert_eq!(config.ws_timeout_secs, 30);
295        assert_eq!(config.update_instruments_interval_mins, 60);
296        assert!(!config.has_credentials());
297    }
298
299    #[rstest]
300    fn test_data_config_has_credentials() {
301        let config = CoinbaseDataClientConfig {
302            api_key: Some("key".into()),
303            api_secret: Some("secret".into()),
304            ..CoinbaseDataClientConfig::default()
305        };
306        assert!(config.has_credentials());
307    }
308
309    #[rstest]
310    fn test_data_config_empty_credentials() {
311        let config = CoinbaseDataClientConfig {
312            api_key: Some("  ".into()),
313            api_secret: Some("secret".into()),
314            ..CoinbaseDataClientConfig::default()
315        };
316        assert!(!config.has_credentials());
317    }
318
319    #[rstest]
320    fn test_data_config_urls_live() {
321        let config = CoinbaseDataClientConfig::default();
322        assert!(config.rest_url().contains("api.coinbase.com"));
323        assert!(config.ws_url().contains("advanced-trade-ws.coinbase.com"));
324    }
325
326    #[rstest]
327    fn test_data_config_urls_sandbox() {
328        let config = CoinbaseDataClientConfig {
329            environment: CoinbaseEnvironment::Sandbox,
330            ..CoinbaseDataClientConfig::default()
331        };
332        assert!(config.rest_url().contains("sandbox"));
333        assert!(config.ws_url().contains("sandbox"));
334    }
335
336    #[rstest]
337    fn test_exec_config_defaults() {
338        let config = CoinbaseExecutionClientConfig::default();
339        assert_eq!(config.environment, CoinbaseEnvironment::Live);
340        assert_eq!(config.http_timeout_secs, 10);
341        assert_eq!(config.max_retries, 3);
342    }
343
344    #[rstest]
345    fn test_exec_config_ws_url_uses_user_endpoint() {
346        let config = CoinbaseExecutionClientConfig::default();
347        assert!(config.ws_url().contains("user"));
348    }
349
350    #[rstest]
351    fn test_data_config_toml_minimal() {
352        let config: CoinbaseDataClientConfig = toml::from_str(
353            r#"
354environment = "Sandbox"
355http_timeout_secs = 5
356update_instruments_interval_mins = 30
357derivatives_poll_interval_secs = 60
358"#,
359        )
360        .unwrap();
361
362        assert_eq!(config.environment, CoinbaseEnvironment::Sandbox);
363        assert_eq!(config.http_timeout_secs, 5);
364        assert_eq!(config.update_instruments_interval_mins, 30);
365        assert_eq!(config.derivatives_poll_interval_secs, 60);
366    }
367
368    #[rstest]
369    fn test_exec_config_toml_empty_uses_defaults() {
370        let config: CoinbaseExecutionClientConfig = toml::from_str("").unwrap();
371        let expected = CoinbaseExecutionClientConfig::default();
372
373        assert_eq!(config.environment, expected.environment);
374        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
375        assert_eq!(config.max_retries, expected.max_retries);
376        assert_eq!(config.account_type, expected.account_type);
377        assert_eq!(config.transport_backend, expected.transport_backend);
378    }
379}