1#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
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(Debug, 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<SecretString>,
43 pub api_secret: Option<SecretString>,
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 pub proxy_url: Option<SecretString>,
55 #[builder(default = true)]
57 pub validate_l3_checksum: bool,
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 KrakenDataClientConfig {
108 pub fn validate(&self) -> anyhow::Result<()> {
114 validate_product_environment(self.product_type, self.environment)
115 }
116
117 pub fn has_api_credentials(&self) -> bool {
119 self.api_key.is_some() && self.api_secret.is_some()
120 }
121
122 pub fn http_base_url(&self) -> String {
124 self.base_url.clone().unwrap_or_else(|| {
125 get_kraken_http_base_url(self.product_type, self.environment).to_string()
126 })
127 }
128
129 pub fn ws_public_url(&self) -> String {
131 self.ws_public_url.clone().unwrap_or_else(|| {
132 get_kraken_ws_public_url(self.product_type, self.environment).to_string()
133 })
134 }
135
136 pub fn ws_private_url(&self) -> String {
138 self.ws_private_url.clone().unwrap_or_else(|| {
139 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
140 })
141 }
142
143 pub fn ws_l3_url(&self) -> String {
145 self.ws_l3_url
146 .clone()
147 .unwrap_or_else(|| crate::common::consts::KRAKEN_SPOT_WS_L3_URL.to_string())
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
153#[serde(default, deny_unknown_fields)]
154#[cfg_attr(
155 feature = "python",
156 pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
157)]
158#[cfg_attr(
159 feature = "python",
160 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
161)]
162pub struct KrakenExecutionClientConfig {
163 #[builder(default = AccountId::from("KRAKEN-001"))]
164 pub account_id: AccountId,
165 #[builder(default)]
166 pub api_key: SecretString,
167 #[builder(default)]
168 pub api_secret: SecretString,
169 #[builder(default = KrakenProductType::Spot)]
170 pub product_type: KrakenProductType,
171 #[builder(default = KrakenEnvironment::Live)]
172 pub environment: KrakenEnvironment,
173 pub base_url: Option<String>,
174 pub ws_url: Option<String>,
175 pub proxy_url: Option<SecretString>,
177 #[builder(default = 30)]
178 pub timeout_secs: u64,
179 #[builder(default = 30)]
180 pub heartbeat_interval_secs: u64,
181 pub auth_timeout_secs: Option<u64>,
184 pub max_requests_per_second: Option<u32>,
185 #[builder(default = 3)]
187 pub max_retries: u32,
188 #[builder(default)]
189 pub transport_backend: TransportBackend,
190
191 #[builder(default = AccountType::Cash)]
197 pub spot_account_type: AccountType,
198
199 pub default_leverage: Option<u16>,
205
206 #[builder(default = false)]
212 pub use_spot_position_reports: bool,
213
214 #[builder(default = "USDT".to_string())]
218 pub spot_positions_quote_currency: String,
219
220 pub margin_balance_asset: Option<String>,
228
229 #[builder(default = true)]
236 pub use_ws_trade: bool,
237
238 #[builder(default = 5)]
244 pub ws_request_timeout_secs: u64,
245}
246
247#[cfg(feature = "python")]
248nautilus_core::impl_pyo3_config_getters!(KrakenExecutionClientConfig {
249 account_id: AccountId,
250 product_type: KrakenProductType,
251 environment: KrakenEnvironment,
252 base_url: Option<String>,
253 timeout_secs: u64,
254 heartbeat_interval_secs: u64,
255 auth_timeout_secs: Option<u64>,
256 max_requests_per_second: Option<u32>,
257 max_retries: u32,
258 spot_account_type: AccountType,
259 default_leverage: Option<u16>,
260 use_spot_position_reports: bool,
261 spot_positions_quote_currency: String,
262 margin_balance_asset: Option<String>,
263 use_ws_trade: bool,
264 ws_request_timeout_secs: u64,
265 transport_backend: TransportBackend,
266});
267
268impl Default for KrakenExecutionClientConfig {
269 fn default() -> Self {
270 Self::builder().build()
271 }
272}
273
274impl KrakenExecutionClientConfig {
275 pub fn http_base_url(&self) -> String {
277 self.base_url.clone().unwrap_or_else(|| {
278 get_kraken_http_base_url(self.product_type, self.environment).to_string()
279 })
280 }
281
282 pub fn ws_url(&self) -> String {
284 self.ws_url.clone().unwrap_or_else(|| {
285 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
286 })
287 }
288
289 pub fn validate(&self) -> anyhow::Result<()> {
296 validate_product_environment(self.product_type, self.environment)?;
297
298 if self.default_leverage.is_some() && self.spot_account_type == AccountType::Cash {
299 anyhow::bail!("default_leverage requires spot_account_type=Margin");
300 }
301 Ok(())
302 }
303}
304
305fn validate_product_environment(
306 product_type: KrakenProductType,
307 environment: KrakenEnvironment,
308) -> anyhow::Result<()> {
309 if product_type == KrakenProductType::Spot && environment == KrakenEnvironment::Demo {
310 anyhow::bail!("Kraken Spot does not support the demo environment");
311 }
312 Ok(())
313}
314
315#[cfg(test)]
316mod tests {
317 use rstest::rstest;
318
319 use super::*;
320
321 const DATA_API_KEY: &str = "data-api-key-sentinel";
322 const DATA_API_SECRET: &str = "data-api-secret-sentinel";
323 const EXEC_API_KEY: &str = "exec-api-key-sentinel";
324 const EXEC_API_SECRET: &str = "exec-api-secret-sentinel";
325
326 #[rstest]
327 fn test_data_config_debug_redacts_credentials() {
328 let config = KrakenDataClientConfig {
329 api_key: Some(DATA_API_KEY.into()),
330 api_secret: Some(DATA_API_SECRET.into()),
331 product_type: KrakenProductType::Futures,
332 timeout_secs: 41,
333 ..Default::default()
334 };
335
336 let debug_output = format!("{config:?}");
337 let api_key_marker = format!("api_key: Some({REDACTED})");
338 let api_secret_marker = format!("api_secret: Some({REDACTED})");
339
340 assert!(!debug_output.contains(DATA_API_KEY));
341 assert!(!debug_output.contains(DATA_API_SECRET));
342 assert!(debug_output.contains(&api_key_marker));
343 assert!(debug_output.contains(&api_secret_marker));
344 assert!(debug_output.contains("product_type: Futures"));
345 assert!(debug_output.contains("timeout_secs: 41"));
346 }
347
348 #[rstest]
349 fn test_exec_config_debug_redacts_credentials() {
350 let config = KrakenExecutionClientConfig {
351 api_key: EXEC_API_KEY.into(),
352 api_secret: EXEC_API_SECRET.into(),
353 product_type: KrakenProductType::Futures,
354 timeout_secs: 43,
355 ..Default::default()
356 };
357
358 let debug_output = format!("{config:?}");
359 let api_key_marker = format!("api_key: {REDACTED}");
360 let api_secret_marker = format!("api_secret: {REDACTED}");
361
362 assert!(!debug_output.contains(EXEC_API_KEY));
363 assert!(!debug_output.contains(EXEC_API_SECRET));
364 assert!(debug_output.contains(&api_key_marker));
365 assert!(debug_output.contains(&api_secret_marker));
366 assert!(debug_output.contains("product_type: Futures"));
367 assert!(debug_output.contains("timeout_secs: 43"));
368 }
369
370 #[rstest]
371 fn test_exec_config_ws_trade_defaults() {
372 let cfg = KrakenExecutionClientConfig::default();
373 assert!(cfg.use_ws_trade);
374 assert_eq!(cfg.ws_request_timeout_secs, 5);
375 assert_eq!(cfg.max_retries, 3);
376 }
377
378 #[rstest]
379 fn test_exec_config_max_retries_serde_round_trip() {
380 let config = KrakenExecutionClientConfig {
381 max_retries: 0,
382 ..Default::default()
383 };
384
385 let serialized = serde_json::to_string(&config).unwrap();
386 let deserialized: KrakenExecutionClientConfig = serde_json::from_str(&serialized).unwrap();
387
388 assert_eq!(deserialized.max_retries, 0);
389 }
390
391 #[rstest]
392 fn test_data_config_toml_minimal() {
393 let config: KrakenDataClientConfig = toml::from_str(
394 r#"
395product_type = "spot"
396environment = "live"
397timeout_secs = 45
398validate_l3_checksum = false
399"#,
400 )
401 .unwrap();
402
403 assert_eq!(config.product_type, KrakenProductType::Spot);
404 assert_eq!(config.environment, KrakenEnvironment::Live);
405 assert_eq!(config.timeout_secs, 45);
406 assert!(!config.validate_l3_checksum);
407 }
408
409 #[rstest]
410 fn test_data_config_ws_idle_timeout_default() {
411 let config = KrakenDataClientConfig::default();
412 assert_eq!(config.ws_idle_timeout_ms, 10_000);
413 }
414
415 #[rstest]
416 fn test_data_config_ws_idle_timeout_override() {
417 let config: KrakenDataClientConfig = toml::from_str("ws_idle_timeout_ms = 0").unwrap();
418
419 assert_eq!(config.ws_idle_timeout_ms, 0);
420 }
421
422 #[rstest]
423 fn test_exec_config_toml_empty_uses_defaults() {
424 let config: KrakenExecutionClientConfig = toml::from_str("").unwrap();
425 let expected = KrakenExecutionClientConfig::default();
426 assert_eq!(config.account_id, expected.account_id);
427 assert_eq!(config.product_type, expected.product_type);
428 assert_eq!(config.environment, expected.environment);
429 assert_eq!(config.timeout_secs, expected.timeout_secs);
430 assert_eq!(config.max_retries, 3);
431 assert_eq!(config.spot_account_type, expected.spot_account_type);
432 assert_eq!(
433 config.use_spot_position_reports,
434 expected.use_spot_position_reports,
435 );
436 assert_eq!(config.use_ws_trade, expected.use_ws_trade);
437 assert_eq!(config.transport_backend, expected.transport_backend);
438 }
439}