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