1use 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#[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 pub api_key: Option<String>,
41 pub api_secret: Option<String>,
43 pub base_url_rest: Option<String>,
45 pub base_url_ws: Option<String>,
47 pub proxy_url: Option<String>,
49 #[builder(default)]
51 pub environment: CoinbaseEnvironment,
52 #[builder(default = 10)]
54 pub http_timeout_secs: u64,
55 #[builder(default = 30)]
57 pub ws_timeout_secs: u64,
58 #[builder(default = 60)]
60 pub update_instruments_interval_mins: u64,
61 #[builder(default = 15)]
66 pub derivatives_poll_interval_secs: u64,
67 #[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 #[must_use]
93 pub fn new() -> Self {
94 Self::default()
95 }
96
97 #[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 #[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 #[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#[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 pub api_key: Option<String>,
140 pub api_secret: Option<String>,
142 pub base_url_rest: Option<String>,
144 pub base_url_ws: Option<String>,
146 pub proxy_url: Option<String>,
148 #[builder(default)]
150 pub environment: CoinbaseEnvironment,
151 #[builder(default = 10)]
153 pub http_timeout_secs: u64,
154 #[builder(default = 3)]
156 pub max_retries: u32,
157 #[builder(default = 100)]
159 pub retry_delay_initial_ms: u64,
160 #[builder(default = 5000)]
162 pub retry_delay_max_ms: u64,
163 #[builder(default = AccountType::Cash)]
166 pub account_type: AccountType,
167 pub default_margin_type: Option<CoinbaseMarginType>,
170 pub default_leverage: Option<rust_decimal::Decimal>,
173 pub retail_portfolio_id: Option<String>,
178 #[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 #[must_use]
208 pub fn new() -> Self {
209 Self::default()
210 }
211
212 #[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 #[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 #[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}