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