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;
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.core.nautilus_pyo3.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.core.nautilus_pyo3.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 CoinbaseExecClientConfig {
138    /// CDP API key name (falls back to `COINBASE_API_KEY` env var).
139    pub api_key: Option<String>,
140    /// CDP API secret in PEM format (falls back to `COINBASE_API_SECRET` env var).
141    pub api_secret: Option<String>,
142    /// Override for the REST API base URL.
143    pub base_url_rest: Option<String>,
144    /// Override for the WebSocket user data URL.
145    pub base_url_ws: Option<String>,
146    /// Optional proxy URL for HTTP and WebSocket transports.
147    pub proxy_url: Option<String>,
148    /// The Coinbase environment to connect to.
149    #[builder(default)]
150    pub environment: CoinbaseEnvironment,
151    /// HTTP timeout in seconds.
152    #[builder(default = 10)]
153    pub http_timeout_secs: u64,
154    /// Maximum number of retry attempts for HTTP requests.
155    #[builder(default = 3)]
156    pub max_retries: u32,
157    /// Initial retry delay in milliseconds.
158    #[builder(default = 100)]
159    pub retry_delay_initial_ms: u64,
160    /// Maximum retry delay in milliseconds.
161    #[builder(default = 5000)]
162    pub retry_delay_max_ms: u64,
163    /// Selects the execution scope: `Cash` for spot, `Margin` for CFM
164    /// derivatives. `CoinbaseExecutionClientFactory` rejects other values.
165    #[builder(default = AccountType::Cash)]
166    pub account_type: AccountType,
167    /// Optional default margin type applied to derivatives orders. Ignored on
168    /// Cash accounts.
169    pub default_margin_type: Option<CoinbaseMarginType>,
170    /// Optional default leverage applied to derivatives orders. Ignored on
171    /// Cash accounts.
172    pub default_leverage: Option<rust_decimal::Decimal>,
173    /// CDP retail portfolio UUID required when the API key is bound to a
174    /// non-default portfolio. When unset, the venue uses the key's default
175    /// portfolio. Coinbase rejects orders with `"account is not available"`
176    /// if the portfolio is non-default and this field is omitted.
177    pub retail_portfolio_id: Option<String>,
178    /// WebSocket transport backend (defaults to `Tungstenite`).
179    #[builder(default)]
180    pub transport_backend: TransportBackend,
181}
182
183#[cfg(feature = "python")]
184nautilus_core::impl_pyo3_config_getters!(CoinbaseExecClientConfig {
185    base_url_rest: Option<String>,
186    base_url_ws: Option<String>,
187    environment: CoinbaseEnvironment,
188    http_timeout_secs: u64,
189    max_retries: u32,
190    retry_delay_initial_ms: u64,
191    retry_delay_max_ms: u64,
192    account_type: AccountType,
193    default_margin_type: Option<CoinbaseMarginType>,
194    default_leverage: Option<rust_decimal::Decimal>,
195    retail_portfolio_id: Option<String>,
196    transport_backend: TransportBackend,
197});
198
199impl Default for CoinbaseExecClientConfig {
200    fn default() -> Self {
201        Self::builder().build()
202    }
203}
204
205impl CoinbaseExecClientConfig {
206    /// Creates a new configuration with default settings.
207    #[must_use]
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Returns true when credentials are populated and non-empty.
213    #[must_use]
214    pub fn has_credentials(&self) -> bool {
215        self.api_key
216            .as_deref()
217            .is_some_and(|s| !s.trim().is_empty())
218            && self
219                .api_secret
220                .as_deref()
221                .is_some_and(|s| !s.trim().is_empty())
222    }
223
224    /// Returns the REST API base URL, respecting environment and overrides.
225    #[must_use]
226    pub fn rest_url(&self) -> String {
227        self.base_url_rest
228            .clone()
229            .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
230    }
231
232    /// Returns the WebSocket user data URL, respecting environment and overrides.
233    #[must_use]
234    pub fn ws_url(&self) -> String {
235        self.base_url_ws
236            .clone()
237            .unwrap_or_else(|| urls::ws_user_url(self.environment).to_string())
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use rstest::rstest;
244
245    use super::*;
246
247    #[rstest]
248    fn test_data_config_defaults() {
249        let config = CoinbaseDataClientConfig::default();
250        assert_eq!(config.environment, CoinbaseEnvironment::Live);
251        assert_eq!(config.http_timeout_secs, 10);
252        assert_eq!(config.ws_timeout_secs, 30);
253        assert_eq!(config.update_instruments_interval_mins, 60);
254        assert!(!config.has_credentials());
255    }
256
257    #[rstest]
258    fn test_data_config_has_credentials() {
259        let config = CoinbaseDataClientConfig {
260            api_key: Some("key".to_string()),
261            api_secret: Some("secret".to_string()),
262            ..CoinbaseDataClientConfig::default()
263        };
264        assert!(config.has_credentials());
265    }
266
267    #[rstest]
268    fn test_data_config_empty_credentials() {
269        let config = CoinbaseDataClientConfig {
270            api_key: Some("  ".to_string()),
271            api_secret: Some("secret".to_string()),
272            ..CoinbaseDataClientConfig::default()
273        };
274        assert!(!config.has_credentials());
275    }
276
277    #[rstest]
278    fn test_data_config_urls_live() {
279        let config = CoinbaseDataClientConfig::default();
280        assert!(config.rest_url().contains("api.coinbase.com"));
281        assert!(config.ws_url().contains("advanced-trade-ws.coinbase.com"));
282    }
283
284    #[rstest]
285    fn test_data_config_urls_sandbox() {
286        let config = CoinbaseDataClientConfig {
287            environment: CoinbaseEnvironment::Sandbox,
288            ..CoinbaseDataClientConfig::default()
289        };
290        assert!(config.rest_url().contains("sandbox"));
291        assert!(config.ws_url().contains("sandbox"));
292    }
293
294    #[rstest]
295    fn test_exec_config_defaults() {
296        let config = CoinbaseExecClientConfig::default();
297        assert_eq!(config.environment, CoinbaseEnvironment::Live);
298        assert_eq!(config.http_timeout_secs, 10);
299        assert_eq!(config.max_retries, 3);
300    }
301
302    #[rstest]
303    fn test_exec_config_ws_url_uses_user_endpoint() {
304        let config = CoinbaseExecClientConfig::default();
305        assert!(config.ws_url().contains("user"));
306    }
307
308    #[rstest]
309    fn test_data_config_toml_minimal() {
310        let config: CoinbaseDataClientConfig = toml::from_str(
311            r#"
312environment = "Sandbox"
313http_timeout_secs = 5
314update_instruments_interval_mins = 30
315derivatives_poll_interval_secs = 60
316"#,
317        )
318        .unwrap();
319
320        assert_eq!(config.environment, CoinbaseEnvironment::Sandbox);
321        assert_eq!(config.http_timeout_secs, 5);
322        assert_eq!(config.update_instruments_interval_mins, 30);
323        assert_eq!(config.derivatives_poll_interval_secs, 60);
324    }
325
326    #[rstest]
327    fn test_exec_config_toml_empty_uses_defaults() {
328        let config: CoinbaseExecClientConfig = toml::from_str("").unwrap();
329        let expected = CoinbaseExecClientConfig::default();
330
331        assert_eq!(config.environment, expected.environment);
332        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
333        assert_eq!(config.max_retries, expected.max_retries);
334        assert_eq!(config.account_type, expected.account_type);
335        assert_eq!(config.transport_backend, expected.transport_backend);
336    }
337}