1use std::fmt::Debug;
19
20#[cfg(test)]
21use nautilus_core::string::secret::REDACTED;
22use nautilus_core::string::secret::SecretString;
23use nautilus_model::{enums::AccountType, identifiers::AccountId};
24use nautilus_network::{url_origin, websocket::TransportBackend};
25use serde::{Deserialize, Serialize};
26
27use crate::common::{
28 enums::{KrakenEnvironment, KrakenProductType},
29 urls::{get_kraken_http_base_url, get_kraken_ws_private_url, get_kraken_ws_public_url},
30};
31
32#[derive(Clone, Serialize, Deserialize, bon::Builder)]
34#[serde(default, deny_unknown_fields)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
42)]
43pub struct KrakenDataClientConfig {
44 pub api_key: Option<SecretString>,
45 pub api_secret: Option<SecretString>,
46 #[builder(default = KrakenProductType::Spot)]
47 pub product_type: KrakenProductType,
48 #[builder(default = KrakenEnvironment::Live)]
49 pub environment: KrakenEnvironment,
50 pub base_url: Option<String>,
51 pub ws_public_url: Option<String>,
52 pub ws_private_url: Option<String>,
53 pub ws_l3_url: Option<String>,
55 pub proxy_url: Option<SecretString>,
57 #[builder(default = true)]
59 pub validate_l3_checksum: bool,
60 #[builder(default = true)]
62 pub validate_l2_checksum: bool,
63 #[builder(default = 30)]
64 pub timeout_secs: u64,
65 #[builder(default = 30)]
66 pub heartbeat_interval_secs: u64,
67 #[builder(default = 10_000)]
87 pub ws_idle_timeout_ms: u64,
88 pub max_requests_per_second: Option<u32>,
89 #[builder(default)]
90 pub transport_backend: TransportBackend,
91}
92
93impl Debug for KrakenDataClientConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct(stringify!(KrakenDataClientConfig))
96 .field("api_key", &self.api_key)
97 .field("api_secret", &self.api_secret)
98 .field("product_type", &self.product_type)
99 .field("environment", &self.environment)
100 .field("base_url", &self.base_url.as_deref().map(url_origin))
101 .field(
102 "ws_public_url",
103 &self.ws_public_url.as_deref().map(url_origin),
104 )
105 .field(
106 "ws_private_url",
107 &self.ws_private_url.as_deref().map(url_origin),
108 )
109 .field("ws_l3_url", &self.ws_l3_url.as_deref().map(url_origin))
110 .field("proxy_url", &self.proxy_url)
111 .field("validate_l3_checksum", &self.validate_l3_checksum)
112 .field("validate_l2_checksum", &self.validate_l2_checksum)
113 .field("timeout_secs", &self.timeout_secs)
114 .field("heartbeat_interval_secs", &self.heartbeat_interval_secs)
115 .field("ws_idle_timeout_ms", &self.ws_idle_timeout_ms)
116 .field("max_requests_per_second", &self.max_requests_per_second)
117 .field("transport_backend", &self.transport_backend)
118 .finish()
119 }
120}
121
122#[cfg(feature = "python")]
123nautilus_core::impl_pyo3_config_getters!(KrakenDataClientConfig {
124 product_type: KrakenProductType,
125 environment: KrakenEnvironment,
126 base_url: Option<String>,
127 validate_l3_checksum: bool,
128 validate_l2_checksum: bool,
129 timeout_secs: u64,
130 heartbeat_interval_secs: u64,
131 ws_idle_timeout_ms: u64,
132 max_requests_per_second: Option<u32>,
133 transport_backend: TransportBackend,
134});
135
136impl Default for KrakenDataClientConfig {
137 fn default() -> Self {
138 Self::builder().build()
139 }
140}
141
142impl KrakenDataClientConfig {
143 pub fn validate(&self) -> anyhow::Result<()> {
149 validate_product_environment(self.product_type, self.environment)
150 }
151
152 pub fn has_api_credentials(&self) -> bool {
154 self.api_key.is_some() && self.api_secret.is_some()
155 }
156
157 pub fn http_base_url(&self) -> String {
159 self.base_url.clone().unwrap_or_else(|| {
160 get_kraken_http_base_url(self.product_type, self.environment).to_string()
161 })
162 }
163
164 pub fn ws_public_url(&self) -> String {
166 self.ws_public_url.clone().unwrap_or_else(|| {
167 get_kraken_ws_public_url(self.product_type, self.environment).to_string()
168 })
169 }
170
171 pub fn ws_private_url(&self) -> String {
173 self.ws_private_url.clone().unwrap_or_else(|| {
174 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
175 })
176 }
177
178 pub fn ws_l3_url(&self) -> String {
180 self.ws_l3_url
181 .clone()
182 .unwrap_or_else(|| crate::common::consts::KRAKEN_SPOT_WS_L3_URL.to_string())
183 }
184}
185
186#[derive(Clone, Serialize, Deserialize, bon::Builder)]
188#[serde(default, deny_unknown_fields)]
189#[cfg_attr(
190 feature = "python",
191 pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
192)]
193#[cfg_attr(
194 feature = "python",
195 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
196)]
197pub struct KrakenExecutionClientConfig {
198 #[builder(default = AccountId::from("KRAKEN-001"))]
199 pub account_id: AccountId,
200 #[builder(default)]
201 pub api_key: SecretString,
202 #[builder(default)]
203 pub api_secret: SecretString,
204 #[builder(default = KrakenProductType::Spot)]
205 pub product_type: KrakenProductType,
206 #[builder(default = KrakenEnvironment::Live)]
207 pub environment: KrakenEnvironment,
208 pub base_url: Option<String>,
209 pub ws_url: Option<String>,
210 pub proxy_url: Option<SecretString>,
212 #[builder(default = 30)]
213 pub timeout_secs: u64,
214 #[builder(default = 30)]
215 pub heartbeat_interval_secs: u64,
216 pub auth_timeout_secs: Option<u64>,
219 pub max_requests_per_second: Option<u32>,
220 #[builder(default = 3)]
222 pub max_retries: u32,
223 #[builder(default)]
224 pub transport_backend: TransportBackend,
225
226 #[builder(default = AccountType::Cash)]
232 pub spot_account_type: AccountType,
233
234 pub default_leverage: Option<u16>,
240
241 #[builder(default = false)]
247 pub use_spot_position_reports: bool,
248
249 #[builder(default = "USDT".to_string())]
256 pub spot_positions_quote_currency: String,
257
258 pub margin_balance_asset: Option<String>,
266
267 #[builder(default = true)]
274 pub use_ws_trade: bool,
275
276 #[builder(default = 5)]
282 pub ws_request_timeout_secs: u64,
283}
284
285impl Debug for KrakenExecutionClientConfig {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 f.debug_struct(stringify!(KrakenExecutionClientConfig))
288 .field("account_id", &self.account_id)
289 .field("api_key", &self.api_key)
290 .field("api_secret", &self.api_secret)
291 .field("product_type", &self.product_type)
292 .field("environment", &self.environment)
293 .field("base_url", &self.base_url.as_deref().map(url_origin))
294 .field("ws_url", &self.ws_url.as_deref().map(url_origin))
295 .field("proxy_url", &self.proxy_url)
296 .field("timeout_secs", &self.timeout_secs)
297 .field("heartbeat_interval_secs", &self.heartbeat_interval_secs)
298 .field("auth_timeout_secs", &self.auth_timeout_secs)
299 .field("max_requests_per_second", &self.max_requests_per_second)
300 .field("max_retries", &self.max_retries)
301 .field("transport_backend", &self.transport_backend)
302 .field("spot_account_type", &self.spot_account_type)
303 .field("default_leverage", &self.default_leverage)
304 .field("use_spot_position_reports", &self.use_spot_position_reports)
305 .field(
306 "spot_positions_quote_currency",
307 &self.spot_positions_quote_currency,
308 )
309 .field("margin_balance_asset", &self.margin_balance_asset)
310 .field("use_ws_trade", &self.use_ws_trade)
311 .field("ws_request_timeout_secs", &self.ws_request_timeout_secs)
312 .finish()
313 }
314}
315
316#[cfg(feature = "python")]
317nautilus_core::impl_pyo3_config_getters!(KrakenExecutionClientConfig {
318 account_id: AccountId,
319 product_type: KrakenProductType,
320 environment: KrakenEnvironment,
321 base_url: Option<String>,
322 timeout_secs: u64,
323 heartbeat_interval_secs: u64,
324 auth_timeout_secs: Option<u64>,
325 max_requests_per_second: Option<u32>,
326 max_retries: u32,
327 spot_account_type: AccountType,
328 default_leverage: Option<u16>,
329 use_spot_position_reports: bool,
330 spot_positions_quote_currency: String,
331 margin_balance_asset: Option<String>,
332 use_ws_trade: bool,
333 ws_request_timeout_secs: u64,
334 transport_backend: TransportBackend,
335});
336
337impl Default for KrakenExecutionClientConfig {
338 fn default() -> Self {
339 Self::builder().build()
340 }
341}
342
343impl KrakenExecutionClientConfig {
344 pub fn http_base_url(&self) -> String {
346 self.base_url.clone().unwrap_or_else(|| {
347 get_kraken_http_base_url(self.product_type, self.environment).to_string()
348 })
349 }
350
351 pub fn ws_url(&self) -> String {
353 self.ws_url.clone().unwrap_or_else(|| {
354 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
355 })
356 }
357
358 pub fn validate(&self) -> anyhow::Result<()> {
365 validate_product_environment(self.product_type, self.environment)?;
366
367 if self.default_leverage.is_some() && self.spot_account_type == AccountType::Cash {
368 anyhow::bail!("default_leverage requires spot_account_type=Margin");
369 }
370 Ok(())
371 }
372}
373
374fn validate_product_environment(
375 product_type: KrakenProductType,
376 environment: KrakenEnvironment,
377) -> anyhow::Result<()> {
378 if product_type == KrakenProductType::Spot && environment == KrakenEnvironment::Demo {
379 anyhow::bail!("Kraken Spot does not support the demo environment");
380 }
381 Ok(())
382}
383
384#[cfg(test)]
385mod tests {
386 use rstest::rstest;
387
388 use super::*;
389
390 const DATA_API_KEY: &str = "data-api-key-sentinel";
391 const DATA_API_SECRET: &str = "data-api-secret-sentinel";
392 const EXEC_API_KEY: &str = "exec-api-key-sentinel";
393 const EXEC_API_SECRET: &str = "exec-api-secret-sentinel";
394
395 #[rstest]
396 fn test_data_config_debug_redacts_credentials() {
397 let config = KrakenDataClientConfig {
398 api_key: Some(DATA_API_KEY.into()),
399 api_secret: Some(DATA_API_SECRET.into()),
400 product_type: KrakenProductType::Futures,
401 timeout_secs: 41,
402 ..Default::default()
403 };
404
405 let debug_output = format!("{config:?}");
406 let api_key_marker = format!("api_key: Some({REDACTED})");
407 let api_secret_marker = format!("api_secret: Some({REDACTED})");
408
409 assert!(!debug_output.contains(DATA_API_KEY));
410 assert!(!debug_output.contains(DATA_API_SECRET));
411 assert!(debug_output.contains(&api_key_marker));
412 assert!(debug_output.contains(&api_secret_marker));
413 assert!(debug_output.contains("product_type: Futures"));
414 assert!(debug_output.contains("timeout_secs: 41"));
415 }
416
417 #[rstest]
418 fn test_exec_config_debug_redacts_credentials() {
419 let config = KrakenExecutionClientConfig {
420 api_key: EXEC_API_KEY.into(),
421 api_secret: EXEC_API_SECRET.into(),
422 product_type: KrakenProductType::Futures,
423 timeout_secs: 43,
424 base_url: Some(
425 "https://SYN-user:SYN-password@api.example.com:8443/SYN-path?token=SYN-query"
426 .to_string(),
427 ),
428 ws_url: Some(
429 "wss://SYN-user:SYN-password@stream.example.com:9443/SYN-path?token=SYN-query"
430 .to_string(),
431 ),
432 ..Default::default()
433 };
434
435 let debug_output = format!("{config:?}");
436 let api_key_marker = format!("api_key: {REDACTED}");
437 let api_secret_marker = format!("api_secret: {REDACTED}");
438
439 assert!(!debug_output.contains(EXEC_API_KEY));
440 assert!(!debug_output.contains(EXEC_API_SECRET));
441 assert!(debug_output.contains(&api_key_marker));
442 assert!(debug_output.contains(&api_secret_marker));
443 assert!(debug_output.contains("product_type: Futures"));
444 assert!(debug_output.contains("timeout_secs: 43"));
445 assert!(debug_output.contains("base_url: Some(\"https://api.example.com:8443\")"));
446 assert!(debug_output.contains("ws_url: Some(\"wss://stream.example.com:9443\")"));
447 assert!(!debug_output.contains("SYN-"));
448 }
449
450 #[rstest]
451 fn test_exec_config_ws_trade_defaults() {
452 let cfg = KrakenExecutionClientConfig::default();
453 assert!(cfg.use_ws_trade);
454 assert_eq!(cfg.ws_request_timeout_secs, 5);
455 assert_eq!(cfg.max_retries, 3);
456 }
457
458 #[rstest]
459 fn test_exec_config_max_retries_serde_round_trip() {
460 let config = KrakenExecutionClientConfig {
461 max_retries: 0,
462 ..Default::default()
463 };
464
465 let serialized = serde_json::to_string(&config).unwrap();
466 let deserialized: KrakenExecutionClientConfig = serde_json::from_str(&serialized).unwrap();
467
468 assert_eq!(deserialized.max_retries, 0);
469 }
470
471 #[rstest]
472 fn test_data_config_toml_minimal() {
473 let config: KrakenDataClientConfig = toml::from_str(
474 r#"
475product_type = "spot"
476environment = "live"
477timeout_secs = 45
478validate_l3_checksum = false
479"#,
480 )
481 .unwrap();
482
483 assert_eq!(config.product_type, KrakenProductType::Spot);
484 assert_eq!(config.environment, KrakenEnvironment::Live);
485 assert_eq!(config.timeout_secs, 45);
486 assert!(!config.validate_l3_checksum);
487 }
488
489 #[rstest]
490 fn test_data_config_ws_idle_timeout_default() {
491 let config = KrakenDataClientConfig::default();
492 assert_eq!(config.ws_idle_timeout_ms, 10_000);
493 }
494
495 #[rstest]
496 fn test_data_config_ws_idle_timeout_override() {
497 let config: KrakenDataClientConfig = toml::from_str("ws_idle_timeout_ms = 0").unwrap();
498
499 assert_eq!(config.ws_idle_timeout_ms, 0);
500 }
501
502 #[rstest]
503 fn test_exec_config_toml_empty_uses_defaults() {
504 let config: KrakenExecutionClientConfig = toml::from_str("").unwrap();
505 let expected = KrakenExecutionClientConfig::default();
506 assert_eq!(config.account_id, expected.account_id);
507 assert_eq!(config.product_type, expected.product_type);
508 assert_eq!(config.environment, expected.environment);
509 assert_eq!(config.timeout_secs, expected.timeout_secs);
510 assert_eq!(config.max_retries, 3);
511 assert_eq!(config.spot_account_type, expected.spot_account_type);
512 assert_eq!(
513 config.use_spot_position_reports,
514 expected.use_spot_position_reports,
515 );
516 assert_eq!(config.use_ws_trade, expected.use_ws_trade);
517 assert_eq!(config.transport_backend, expected.transport_backend);
518 }
519}