1use std::fmt::Debug;
19
20use nautilus_core::string::secret::REDACTED;
21use nautilus_model::{enums::AccountType, identifiers::AccountId};
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{
26 enums::{KrakenEnvironment, KrakenProductType},
27 urls::{get_kraken_http_base_url, get_kraken_ws_private_url, get_kraken_ws_public_url},
28};
29
30#[derive(Clone, Serialize, Deserialize, bon::Builder)]
32#[serde(default, deny_unknown_fields)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
40)]
41pub struct KrakenDataClientConfig {
42 pub api_key: Option<String>,
43 pub api_secret: Option<String>,
44 #[builder(default = KrakenProductType::Spot)]
45 pub product_type: KrakenProductType,
46 #[builder(default = KrakenEnvironment::Live)]
47 pub environment: KrakenEnvironment,
48 pub base_url: Option<String>,
49 pub ws_public_url: Option<String>,
50 pub ws_private_url: Option<String>,
51 pub ws_l3_url: Option<String>,
53 #[builder(default = true)]
55 pub validate_l3_checksum: bool,
56 pub proxy_url: Option<String>,
58 #[builder(default = 30)]
59 pub timeout_secs: u64,
60 #[builder(default = 30)]
61 pub heartbeat_interval_secs: u64,
62 #[builder(default = 10_000)]
82 pub ws_idle_timeout_ms: u64,
83 pub max_requests_per_second: Option<u32>,
84 #[builder(default)]
85 pub transport_backend: TransportBackend,
86}
87
88#[cfg(feature = "python")]
89nautilus_core::impl_pyo3_config_getters!(KrakenDataClientConfig {
90 product_type: KrakenProductType,
91 environment: KrakenEnvironment,
92 base_url: Option<String>,
93 validate_l3_checksum: bool,
94 timeout_secs: u64,
95 heartbeat_interval_secs: u64,
96 ws_idle_timeout_ms: u64,
97 max_requests_per_second: Option<u32>,
98 transport_backend: TransportBackend,
99});
100
101impl Default for KrakenDataClientConfig {
102 fn default() -> Self {
103 Self::builder().build()
104 }
105}
106
107impl Debug for KrakenDataClientConfig {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct(stringify!(KrakenDataClientConfig))
110 .field("api_key", &self.api_key.as_ref().map(|_| REDACTED))
111 .field("api_secret", &self.api_secret.as_ref().map(|_| REDACTED))
112 .field("product_type", &self.product_type)
113 .field("environment", &self.environment)
114 .field("base_url", &self.base_url)
115 .field("ws_public_url", &self.ws_public_url)
116 .field("ws_private_url", &self.ws_private_url)
117 .field("ws_l3_url", &self.ws_l3_url)
118 .field("validate_l3_checksum", &self.validate_l3_checksum)
119 .field("proxy_url", &self.proxy_url)
120 .field("timeout_secs", &self.timeout_secs)
121 .field("heartbeat_interval_secs", &self.heartbeat_interval_secs)
122 .field("ws_idle_timeout_ms", &self.ws_idle_timeout_ms)
123 .field("max_requests_per_second", &self.max_requests_per_second)
124 .field("transport_backend", &self.transport_backend)
125 .finish()
126 }
127}
128
129impl KrakenDataClientConfig {
130 pub fn validate(&self) -> anyhow::Result<()> {
136 validate_product_environment(self.product_type, self.environment)
137 }
138
139 pub fn has_api_credentials(&self) -> bool {
141 self.api_key.is_some() && self.api_secret.is_some()
142 }
143
144 pub fn http_base_url(&self) -> String {
146 self.base_url.clone().unwrap_or_else(|| {
147 get_kraken_http_base_url(self.product_type, self.environment).to_string()
148 })
149 }
150
151 pub fn ws_public_url(&self) -> String {
153 self.ws_public_url.clone().unwrap_or_else(|| {
154 get_kraken_ws_public_url(self.product_type, self.environment).to_string()
155 })
156 }
157
158 pub fn ws_private_url(&self) -> String {
160 self.ws_private_url.clone().unwrap_or_else(|| {
161 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
162 })
163 }
164
165 pub fn ws_l3_url(&self) -> String {
167 self.ws_l3_url
168 .clone()
169 .unwrap_or_else(|| crate::common::consts::KRAKEN_SPOT_WS_L3_URL.to_string())
170 }
171}
172
173#[derive(Clone, Serialize, Deserialize, bon::Builder)]
175#[serde(default, deny_unknown_fields)]
176#[cfg_attr(
177 feature = "python",
178 pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
179)]
180#[cfg_attr(
181 feature = "python",
182 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
183)]
184pub struct KrakenExecutionClientConfig {
185 #[builder(default = AccountId::from("KRAKEN-001"))]
186 pub account_id: AccountId,
187 #[builder(default)]
188 pub api_key: String,
189 #[builder(default)]
190 pub api_secret: String,
191 #[builder(default = KrakenProductType::Spot)]
192 pub product_type: KrakenProductType,
193 #[builder(default = KrakenEnvironment::Live)]
194 pub environment: KrakenEnvironment,
195 pub base_url: Option<String>,
196 pub ws_url: Option<String>,
197 pub proxy_url: Option<String>,
199 #[builder(default = 30)]
200 pub timeout_secs: u64,
201 #[builder(default = 30)]
202 pub heartbeat_interval_secs: u64,
203 pub auth_timeout_secs: Option<u64>,
206 pub max_requests_per_second: Option<u32>,
207 #[builder(default)]
208 pub transport_backend: TransportBackend,
209
210 #[builder(default = AccountType::Cash)]
216 pub spot_account_type: AccountType,
217
218 pub default_leverage: Option<u16>,
224
225 #[builder(default = false)]
231 pub use_spot_position_reports: bool,
232
233 #[builder(default = "USDT".to_string())]
237 pub spot_positions_quote_currency: String,
238
239 pub margin_balance_asset: Option<String>,
247
248 #[builder(default = true)]
255 pub use_ws_trade: bool,
256
257 #[builder(default = 5)]
263 pub ws_request_timeout_secs: u64,
264}
265
266#[cfg(feature = "python")]
267nautilus_core::impl_pyo3_config_getters!(KrakenExecutionClientConfig {
268 account_id: AccountId,
269 product_type: KrakenProductType,
270 environment: KrakenEnvironment,
271 base_url: Option<String>,
272 timeout_secs: u64,
273 heartbeat_interval_secs: u64,
274 auth_timeout_secs: Option<u64>,
275 max_requests_per_second: Option<u32>,
276 spot_account_type: AccountType,
277 default_leverage: Option<u16>,
278 use_spot_position_reports: bool,
279 spot_positions_quote_currency: String,
280 margin_balance_asset: Option<String>,
281 use_ws_trade: bool,
282 ws_request_timeout_secs: u64,
283 transport_backend: TransportBackend,
284});
285
286impl Default for KrakenExecutionClientConfig {
287 fn default() -> Self {
288 Self::builder().build()
289 }
290}
291
292impl Debug for KrakenExecutionClientConfig {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct(stringify!(KrakenExecutionClientConfig))
295 .field("account_id", &self.account_id)
296 .field("api_key", &REDACTED)
297 .field("api_secret", &REDACTED)
298 .field("product_type", &self.product_type)
299 .field("environment", &self.environment)
300 .field("base_url", &self.base_url)
301 .field("ws_url", &self.ws_url)
302 .field("proxy_url", &self.proxy_url)
303 .field("timeout_secs", &self.timeout_secs)
304 .field("heartbeat_interval_secs", &self.heartbeat_interval_secs)
305 .field("auth_timeout_secs", &self.auth_timeout_secs)
306 .field("max_requests_per_second", &self.max_requests_per_second)
307 .field("transport_backend", &self.transport_backend)
308 .field("spot_account_type", &self.spot_account_type)
309 .field("default_leverage", &self.default_leverage)
310 .field("use_spot_position_reports", &self.use_spot_position_reports)
311 .field(
312 "spot_positions_quote_currency",
313 &self.spot_positions_quote_currency,
314 )
315 .field("margin_balance_asset", &self.margin_balance_asset)
316 .field("use_ws_trade", &self.use_ws_trade)
317 .field("ws_request_timeout_secs", &self.ws_request_timeout_secs)
318 .finish()
319 }
320}
321
322impl KrakenExecutionClientConfig {
323 pub fn http_base_url(&self) -> String {
325 self.base_url.clone().unwrap_or_else(|| {
326 get_kraken_http_base_url(self.product_type, self.environment).to_string()
327 })
328 }
329
330 pub fn ws_url(&self) -> String {
332 self.ws_url.clone().unwrap_or_else(|| {
333 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
334 })
335 }
336
337 pub fn validate(&self) -> anyhow::Result<()> {
344 validate_product_environment(self.product_type, self.environment)?;
345
346 if self.default_leverage.is_some() && self.spot_account_type == AccountType::Cash {
347 anyhow::bail!("default_leverage requires spot_account_type=Margin");
348 }
349 Ok(())
350 }
351}
352
353fn validate_product_environment(
354 product_type: KrakenProductType,
355 environment: KrakenEnvironment,
356) -> anyhow::Result<()> {
357 if product_type == KrakenProductType::Spot && environment == KrakenEnvironment::Demo {
358 anyhow::bail!("Kraken Spot does not support the demo environment");
359 }
360 Ok(())
361}
362
363#[cfg(test)]
364mod tests {
365 use rstest::rstest;
366
367 use super::*;
368
369 const DATA_API_KEY: &str = "data-api-key-sentinel";
370 const DATA_API_SECRET: &str = "data-api-secret-sentinel";
371 const EXEC_API_KEY: &str = "exec-api-key-sentinel";
372 const EXEC_API_SECRET: &str = "exec-api-secret-sentinel";
373
374 #[rstest]
375 fn test_data_config_debug_redacts_credentials() {
376 let config = KrakenDataClientConfig {
377 api_key: Some(DATA_API_KEY.to_string()),
378 api_secret: Some(DATA_API_SECRET.to_string()),
379 product_type: KrakenProductType::Futures,
380 timeout_secs: 41,
381 ..Default::default()
382 };
383
384 let debug_output = format!("{config:?}");
385 let api_key_marker = format!("api_key: Some({REDACTED:?})");
386 let api_secret_marker = format!("api_secret: Some({REDACTED:?})");
387
388 assert!(!debug_output.contains(DATA_API_KEY));
389 assert!(!debug_output.contains(DATA_API_SECRET));
390 assert!(debug_output.contains(&api_key_marker));
391 assert!(debug_output.contains(&api_secret_marker));
392 assert!(debug_output.contains("product_type: Futures"));
393 assert!(debug_output.contains("timeout_secs: 41"));
394 }
395
396 #[rstest]
397 fn test_exec_config_debug_redacts_credentials() {
398 let config = KrakenExecutionClientConfig {
399 api_key: EXEC_API_KEY.to_string(),
400 api_secret: EXEC_API_SECRET.to_string(),
401 product_type: KrakenProductType::Futures,
402 timeout_secs: 43,
403 ..Default::default()
404 };
405
406 let debug_output = format!("{config:?}");
407 let api_key_marker = format!("api_key: {REDACTED:?}");
408 let api_secret_marker = format!("api_secret: {REDACTED:?}");
409
410 assert!(!debug_output.contains(EXEC_API_KEY));
411 assert!(!debug_output.contains(EXEC_API_SECRET));
412 assert!(debug_output.contains(&api_key_marker));
413 assert!(debug_output.contains(&api_secret_marker));
414 assert!(debug_output.contains("product_type: Futures"));
415 assert!(debug_output.contains("timeout_secs: 43"));
416 }
417
418 #[rstest]
419 fn test_exec_config_ws_trade_defaults() {
420 let cfg = KrakenExecutionClientConfig::default();
421 assert!(cfg.use_ws_trade);
422 assert_eq!(cfg.ws_request_timeout_secs, 5);
423 }
424
425 #[rstest]
426 fn test_data_config_toml_minimal() {
427 let config: KrakenDataClientConfig = toml::from_str(
428 r#"
429product_type = "spot"
430environment = "live"
431timeout_secs = 45
432validate_l3_checksum = false
433"#,
434 )
435 .unwrap();
436
437 assert_eq!(config.product_type, KrakenProductType::Spot);
438 assert_eq!(config.environment, KrakenEnvironment::Live);
439 assert_eq!(config.timeout_secs, 45);
440 assert!(!config.validate_l3_checksum);
441 }
442
443 #[rstest]
444 fn test_data_config_ws_idle_timeout_default() {
445 let config = KrakenDataClientConfig::default();
446 assert_eq!(config.ws_idle_timeout_ms, 10_000);
447 }
448
449 #[rstest]
450 fn test_data_config_ws_idle_timeout_override() {
451 let config: KrakenDataClientConfig = toml::from_str("ws_idle_timeout_ms = 0").unwrap();
452
453 assert_eq!(config.ws_idle_timeout_ms, 0);
454 }
455
456 #[rstest]
457 fn test_exec_config_toml_empty_uses_defaults() {
458 let config: KrakenExecutionClientConfig = toml::from_str("").unwrap();
459 let expected = KrakenExecutionClientConfig::default();
460 assert_eq!(config.account_id, expected.account_id);
461 assert_eq!(config.product_type, expected.product_type);
462 assert_eq!(config.environment, expected.environment);
463 assert_eq!(config.timeout_secs, expected.timeout_secs);
464 assert_eq!(config.spot_account_type, expected.spot_account_type);
465 assert_eq!(
466 config.use_spot_position_reports,
467 expected.use_spot_position_reports,
468 );
469 assert_eq!(config.use_ws_trade, expected.use_ws_trade);
470 assert_eq!(config.transport_backend, expected.transport_backend);
471 }
472}