1use 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#[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 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.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 #[builder(default = AccountId::from("COINBASE-001"))]
140 pub account_id: AccountId,
141 pub api_key: Option<String>,
143 pub api_secret: Option<String>,
145 pub base_url_rest: Option<String>,
147 pub base_url_ws: Option<String>,
149 pub proxy_url: Option<String>,
151 #[builder(default)]
153 pub environment: CoinbaseEnvironment,
154 #[builder(default = 10)]
156 pub http_timeout_secs: u64,
157 #[builder(default = 3)]
159 pub max_retries: u32,
160 #[builder(default = 100)]
162 pub retry_delay_initial_ms: u64,
163 #[builder(default = 5000)]
165 pub retry_delay_max_ms: u64,
166 #[builder(default = AccountType::Cash)]
169 pub account_type: AccountType,
170 pub default_margin_type: Option<CoinbaseMarginType>,
173 pub default_leverage: Option<rust_decimal::Decimal>,
176 pub retail_portfolio_id: Option<String>,
181 #[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 #[must_use]
212 pub fn new() -> Self {
213 Self::default()
214 }
215
216 #[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 #[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 #[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}