1use nautilus_model::{
19 enums::AccountType,
20 identifiers::{AccountId, TraderId},
21};
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.core.nautilus_pyo3.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
88impl Default for KrakenDataClientConfig {
89 fn default() -> Self {
90 Self::builder().build()
91 }
92}
93
94impl KrakenDataClientConfig {
95 pub fn validate(&self) -> anyhow::Result<()> {
101 validate_product_environment(self.product_type, self.environment)
102 }
103
104 pub fn has_api_credentials(&self) -> bool {
106 self.api_key.is_some() && self.api_secret.is_some()
107 }
108
109 pub fn http_base_url(&self) -> String {
111 self.base_url.clone().unwrap_or_else(|| {
112 get_kraken_http_base_url(self.product_type, self.environment).to_string()
113 })
114 }
115
116 pub fn ws_public_url(&self) -> String {
118 self.ws_public_url.clone().unwrap_or_else(|| {
119 get_kraken_ws_public_url(self.product_type, self.environment).to_string()
120 })
121 }
122
123 pub fn ws_private_url(&self) -> String {
125 self.ws_private_url.clone().unwrap_or_else(|| {
126 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
127 })
128 }
129
130 pub fn ws_l3_url(&self) -> String {
132 self.ws_l3_url
133 .clone()
134 .unwrap_or_else(|| crate::common::consts::KRAKEN_SPOT_WS_L3_URL.to_string())
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
140#[serde(default, deny_unknown_fields)]
141#[cfg_attr(
142 feature = "python",
143 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.kraken", from_py_object)
144)]
145#[cfg_attr(
146 feature = "python",
147 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
148)]
149pub struct KrakenExecClientConfig {
150 #[builder(default)]
151 pub trader_id: TraderId,
152 #[builder(default = AccountId::from("KRAKEN-001"))]
153 pub account_id: AccountId,
154 #[builder(default)]
155 pub api_key: String,
156 #[builder(default)]
157 pub api_secret: String,
158 #[builder(default = KrakenProductType::Spot)]
159 pub product_type: KrakenProductType,
160 #[builder(default = KrakenEnvironment::Live)]
161 pub environment: KrakenEnvironment,
162 pub base_url: Option<String>,
163 pub ws_url: Option<String>,
164 pub proxy_url: Option<String>,
166 #[builder(default = 30)]
167 pub timeout_secs: u64,
168 #[builder(default = 30)]
169 pub heartbeat_interval_secs: u64,
170 pub max_requests_per_second: Option<u32>,
171 #[builder(default)]
172 pub transport_backend: TransportBackend,
173
174 #[builder(default = AccountType::Cash)]
180 pub spot_account_type: AccountType,
181
182 pub default_leverage: Option<u16>,
188
189 #[builder(default = false)]
195 pub use_spot_position_reports: bool,
196
197 #[builder(default = "USDT".to_string())]
201 pub spot_positions_quote_currency: String,
202
203 pub margin_balance_asset: Option<String>,
211
212 #[builder(default = true)]
219 pub use_ws_trade: bool,
220
221 #[builder(default = 5)]
226 pub ws_request_timeout_secs: u64,
227}
228
229impl Default for KrakenExecClientConfig {
230 fn default() -> Self {
231 Self::builder().build()
232 }
233}
234
235impl KrakenExecClientConfig {
236 pub fn http_base_url(&self) -> String {
238 self.base_url.clone().unwrap_or_else(|| {
239 get_kraken_http_base_url(self.product_type, self.environment).to_string()
240 })
241 }
242
243 pub fn ws_url(&self) -> String {
245 self.ws_url.clone().unwrap_or_else(|| {
246 get_kraken_ws_private_url(self.product_type, self.environment).to_string()
247 })
248 }
249
250 pub fn validate(&self) -> anyhow::Result<()> {
257 validate_product_environment(self.product_type, self.environment)?;
258
259 if self.default_leverage.is_some() && self.spot_account_type == AccountType::Cash {
260 anyhow::bail!("default_leverage requires spot_account_type=Margin");
261 }
262 Ok(())
263 }
264}
265
266fn validate_product_environment(
267 product_type: KrakenProductType,
268 environment: KrakenEnvironment,
269) -> anyhow::Result<()> {
270 if product_type == KrakenProductType::Spot && environment == KrakenEnvironment::Demo {
271 anyhow::bail!("Kraken Spot does not support the demo environment");
272 }
273 Ok(())
274}
275
276#[cfg(test)]
277mod tests {
278 use rstest::rstest;
279
280 use super::*;
281
282 #[rstest]
283 fn test_exec_config_ws_trade_defaults() {
284 let cfg = KrakenExecClientConfig::default();
285 assert!(cfg.use_ws_trade);
286 assert_eq!(cfg.ws_request_timeout_secs, 5);
287 }
288
289 #[rstest]
290 fn test_data_config_toml_minimal() {
291 let config: KrakenDataClientConfig = toml::from_str(
292 r#"
293product_type = "spot"
294environment = "live"
295timeout_secs = 45
296validate_l3_checksum = false
297"#,
298 )
299 .unwrap();
300
301 assert_eq!(config.product_type, KrakenProductType::Spot);
302 assert_eq!(config.environment, KrakenEnvironment::Live);
303 assert_eq!(config.timeout_secs, 45);
304 assert!(!config.validate_l3_checksum);
305 }
306
307 #[rstest]
308 fn test_data_config_ws_idle_timeout_default() {
309 let config = KrakenDataClientConfig::default();
310 assert_eq!(config.ws_idle_timeout_ms, 10_000);
311 }
312
313 #[rstest]
314 fn test_data_config_ws_idle_timeout_override() {
315 let config: KrakenDataClientConfig = toml::from_str("ws_idle_timeout_ms = 0").unwrap();
316
317 assert_eq!(config.ws_idle_timeout_ms, 0);
318 }
319
320 #[rstest]
321 fn test_exec_config_toml_empty_uses_defaults() {
322 let config: KrakenExecClientConfig = toml::from_str("").unwrap();
323 let expected = KrakenExecClientConfig::default();
324
325 assert_eq!(config.trader_id, expected.trader_id);
326 assert_eq!(config.account_id, expected.account_id);
327 assert_eq!(config.product_type, expected.product_type);
328 assert_eq!(config.environment, expected.environment);
329 assert_eq!(config.timeout_secs, expected.timeout_secs);
330 assert_eq!(config.spot_account_type, expected.spot_account_type);
331 assert_eq!(
332 config.use_spot_position_reports,
333 expected.use_spot_position_reports,
334 );
335 assert_eq!(config.use_ws_trade, expected.use_ws_trade);
336 assert_eq!(config.transport_backend, expected.transport_backend);
337 }
338}