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
72impl Default for CoinbaseDataClientConfig {
73    fn default() -> Self {
74        Self::builder().build()
75    }
76}
77
78impl CoinbaseDataClientConfig {
79    /// Creates a new configuration with default settings.
80    #[must_use]
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Returns true when credentials are populated and non-empty.
86    #[must_use]
87    pub fn has_credentials(&self) -> bool {
88        self.api_key
89            .as_deref()
90            .is_some_and(|s| !s.trim().is_empty())
91            && self
92                .api_secret
93                .as_deref()
94                .is_some_and(|s| !s.trim().is_empty())
95    }
96
97    /// Returns the REST API base URL, respecting environment and overrides.
98    #[must_use]
99    pub fn rest_url(&self) -> String {
100        self.base_url_rest
101            .clone()
102            .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
103    }
104
105    /// Returns the WebSocket market data URL, respecting environment and overrides.
106    #[must_use]
107    pub fn ws_url(&self) -> String {
108        self.base_url_ws
109            .clone()
110            .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
111    }
112}
113
114/// Configuration for the Coinbase execution client.
115#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
116#[serde(default, deny_unknown_fields)]
117#[cfg_attr(
118    feature = "python",
119    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.coinbase", from_py_object)
120)]
121#[cfg_attr(
122    feature = "python",
123    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinbase")
124)]
125pub struct CoinbaseExecClientConfig {
126    /// CDP API key name (falls back to `COINBASE_API_KEY` env var).
127    pub api_key: Option<String>,
128    /// CDP API secret in PEM format (falls back to `COINBASE_API_SECRET` env var).
129    pub api_secret: Option<String>,
130    /// Override for the REST API base URL.
131    pub base_url_rest: Option<String>,
132    /// Override for the WebSocket user data URL.
133    pub base_url_ws: Option<String>,
134    /// Optional proxy URL for HTTP and WebSocket transports.
135    pub proxy_url: Option<String>,
136    /// The Coinbase environment to connect to.
137    #[builder(default)]
138    pub environment: CoinbaseEnvironment,
139    /// HTTP timeout in seconds.
140    #[builder(default = 10)]
141    pub http_timeout_secs: u64,
142    /// Maximum number of retry attempts for HTTP requests.
143    #[builder(default = 3)]
144    pub max_retries: u32,
145    /// Initial retry delay in milliseconds.
146    #[builder(default = 100)]
147    pub retry_delay_initial_ms: u64,
148    /// Maximum retry delay in milliseconds.
149    #[builder(default = 5000)]
150    pub retry_delay_max_ms: u64,
151    /// Selects the execution scope: `Cash` for spot, `Margin` for CFM
152    /// derivatives. `CoinbaseExecutionClientFactory` rejects other values.
153    #[builder(default = AccountType::Cash)]
154    pub account_type: AccountType,
155    /// Optional default margin type applied to derivatives orders. Ignored on
156    /// Cash accounts.
157    pub default_margin_type: Option<CoinbaseMarginType>,
158    /// Optional default leverage applied to derivatives orders. Ignored on
159    /// Cash accounts.
160    pub default_leverage: Option<rust_decimal::Decimal>,
161    /// CDP retail portfolio UUID required when the API key is bound to a
162    /// non-default portfolio. When unset, the venue uses the key's default
163    /// portfolio. Coinbase rejects orders with `"account is not available"`
164    /// if the portfolio is non-default and this field is omitted.
165    pub retail_portfolio_id: Option<String>,
166    /// WebSocket transport backend (defaults to `Tungstenite`).
167    #[builder(default)]
168    pub transport_backend: TransportBackend,
169}
170
171impl Default for CoinbaseExecClientConfig {
172    fn default() -> Self {
173        Self::builder().build()
174    }
175}
176
177impl CoinbaseExecClientConfig {
178    /// Creates a new configuration with default settings.
179    #[must_use]
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Returns true when credentials are populated and non-empty.
185    #[must_use]
186    pub fn has_credentials(&self) -> bool {
187        self.api_key
188            .as_deref()
189            .is_some_and(|s| !s.trim().is_empty())
190            && self
191                .api_secret
192                .as_deref()
193                .is_some_and(|s| !s.trim().is_empty())
194    }
195
196    /// Returns the REST API base URL, respecting environment and overrides.
197    #[must_use]
198    pub fn rest_url(&self) -> String {
199        self.base_url_rest
200            .clone()
201            .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
202    }
203
204    /// Returns the WebSocket user data URL, respecting environment and overrides.
205    #[must_use]
206    pub fn ws_url(&self) -> String {
207        self.base_url_ws
208            .clone()
209            .unwrap_or_else(|| urls::ws_user_url(self.environment).to_string())
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use rstest::rstest;
216
217    use super::*;
218
219    #[rstest]
220    fn test_data_config_defaults() {
221        let config = CoinbaseDataClientConfig::default();
222        assert_eq!(config.environment, CoinbaseEnvironment::Live);
223        assert_eq!(config.http_timeout_secs, 10);
224        assert_eq!(config.ws_timeout_secs, 30);
225        assert_eq!(config.update_instruments_interval_mins, 60);
226        assert!(!config.has_credentials());
227    }
228
229    #[rstest]
230    fn test_data_config_has_credentials() {
231        let config = CoinbaseDataClientConfig {
232            api_key: Some("key".to_string()),
233            api_secret: Some("secret".to_string()),
234            ..CoinbaseDataClientConfig::default()
235        };
236        assert!(config.has_credentials());
237    }
238
239    #[rstest]
240    fn test_data_config_empty_credentials() {
241        let config = CoinbaseDataClientConfig {
242            api_key: Some("  ".to_string()),
243            api_secret: Some("secret".to_string()),
244            ..CoinbaseDataClientConfig::default()
245        };
246        assert!(!config.has_credentials());
247    }
248
249    #[rstest]
250    fn test_data_config_urls_live() {
251        let config = CoinbaseDataClientConfig::default();
252        assert!(config.rest_url().contains("api.coinbase.com"));
253        assert!(config.ws_url().contains("advanced-trade-ws.coinbase.com"));
254    }
255
256    #[rstest]
257    fn test_data_config_urls_sandbox() {
258        let config = CoinbaseDataClientConfig {
259            environment: CoinbaseEnvironment::Sandbox,
260            ..CoinbaseDataClientConfig::default()
261        };
262        assert!(config.rest_url().contains("sandbox"));
263        assert!(config.ws_url().contains("sandbox"));
264    }
265
266    #[rstest]
267    fn test_exec_config_defaults() {
268        let config = CoinbaseExecClientConfig::default();
269        assert_eq!(config.environment, CoinbaseEnvironment::Live);
270        assert_eq!(config.http_timeout_secs, 10);
271        assert_eq!(config.max_retries, 3);
272    }
273
274    #[rstest]
275    fn test_exec_config_ws_url_uses_user_endpoint() {
276        let config = CoinbaseExecClientConfig::default();
277        assert!(config.ws_url().contains("user"));
278    }
279
280    #[rstest]
281    fn test_data_config_toml_minimal() {
282        let config: CoinbaseDataClientConfig = toml::from_str(
283            r#"
284environment = "Sandbox"
285http_timeout_secs = 5
286update_instruments_interval_mins = 30
287derivatives_poll_interval_secs = 60
288"#,
289        )
290        .unwrap();
291
292        assert_eq!(config.environment, CoinbaseEnvironment::Sandbox);
293        assert_eq!(config.http_timeout_secs, 5);
294        assert_eq!(config.update_instruments_interval_mins, 30);
295        assert_eq!(config.derivatives_poll_interval_secs, 60);
296    }
297
298    #[rstest]
299    fn test_exec_config_toml_empty_uses_defaults() {
300        let config: CoinbaseExecClientConfig = toml::from_str("").unwrap();
301        let expected = CoinbaseExecClientConfig::default();
302
303        assert_eq!(config.environment, expected.environment);
304        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
305        assert_eq!(config.max_retries, expected.max_retries);
306        assert_eq!(config.account_type, expected.account_type);
307        assert_eq!(config.transport_backend, expected.transport_backend);
308    }
309}