Skip to main content

nautilus_kraken/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration types for Kraken data and execution clients.
17
18use 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/// Configuration for the Kraken data client.
31#[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    /// Override for the L3 WebSocket URL. Defaults to `wss://ws-l3.kraken.com/v2`.
52    pub ws_l3_url: Option<String>,
53    /// Validate Kraken's CRC32 checksum on each L3 update.
54    #[builder(default = true)]
55    pub validate_l3_checksum: bool,
56    /// Optional proxy URL for HTTP and WebSocket transports.
57    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    /// Idle timeout (milliseconds) for the spot v2 WebSocket.
63    ///
64    /// If no application data (any text or binary frame) is received within this
65    /// window, the connection is treated as dead and the client reconnects and
66    /// resubscribes. This recovers from a backend that acknowledges a
67    /// subscription but never attaches the data fan-out: the socket stays open
68    /// with no close frame or transport error, so nothing else detects it.
69    ///
70    /// Kraken sends a `heartbeat` text frame once per second while at least one
71    /// subscription is active, so a live subscribed connection resets this timer
72    /// well within the window. Note the client's keepalive `ping` is answered
73    /// with a `pong` *text* frame, which also resets the timer roughly every
74    /// `heartbeat_interval_secs`; the default below is therefore kept short
75    /// enough to rely on the 1/s heartbeats rather than the keepalive, which
76    /// assumes the connection carries at least one subscription. A connection
77    /// held open without any subscription should disable this (`0`) or raise it
78    /// above `heartbeat_interval_secs`.
79    ///
80    /// `0` disables the idle timeout.
81    #[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    /// Validates config invariants.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the demo environment is used for Spot.
100    pub fn validate(&self) -> anyhow::Result<()> {
101        validate_product_environment(self.product_type, self.environment)
102    }
103
104    /// Returns true if both API key and secret are set.
105    pub fn has_api_credentials(&self) -> bool {
106        self.api_key.is_some() && self.api_secret.is_some()
107    }
108
109    /// Returns the HTTP base URL for the configured product type and environment.
110    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    /// Returns the public WebSocket URL for the configured product type and environment.
117    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    /// Returns the private WebSocket URL for the configured product type and environment.
124    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    /// Returns the L3 WebSocket URL for the configured environment.
131    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/// Configuration for the Kraken execution client.
139#[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    /// Optional proxy URL for HTTP and WebSocket transports.
165    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    /// Account type for spot trading (`Cash` or `Margin`).
175    ///
176    /// When set to `Margin`, the adapter calls `TradeBalance` for margin reporting
177    /// and `OpenPositions` for position reconciliation.
178    /// Per-order leverage is set via `SubmitOrder.params["leverage"]` (u16 multiplier).
179    #[builder(default = AccountType::Cash)]
180    pub spot_account_type: AccountType,
181
182    /// Default leverage multiplier for spot margin orders when not overridden per-order.
183    ///
184    /// Sent as `"N:1"` to Kraken (e.g., `3` becomes `"3:1"`).
185    /// Valid tiers per pair are in `AssetPairInfo.leverage_buy` / `leverage_sell`.
186    /// `None` means cash orders (no leverage field sent).
187    pub default_leverage: Option<u16>,
188
189    /// Whether to generate `PositionStatusReport`s from spot wallet balances.
190    ///
191    /// Set `true` for spot-only (cash) accounts that need position tracking from
192    /// balance snapshots. For margin accounts leave `false`; positions are
193    /// reconciled via `OpenPositions` instead.
194    #[builder(default = false)]
195    pub use_spot_position_reports: bool,
196
197    /// Quote currency used for synthetic spot position reports.
198    ///
199    /// Only relevant when `use_spot_position_reports` is `true`.
200    #[builder(default = "USDT".to_string())]
201    pub spot_positions_quote_currency: String,
202
203    /// Summary-display asset for `TradeBalance` margin metrics.
204    ///
205    /// Controls the denomination of equity, free margin, used margin, and other
206    /// summary figures returned by Kraken's `TradeBalance` endpoint (e.g. `"ZUSD"`,
207    /// `"ZGBP"`, `"ZEUR"`, `"USDT"`). `None` lets Kraken default to `ZUSD`.
208    /// Display-only: Kraken converts internally; per-position figures from
209    /// `OpenPositions` remain in the traded pair's quote currency.
210    pub margin_balance_asset: Option<String>,
211
212    /// Use WebSocket v2 for order submission, modification, and cancellation.
213    ///
214    /// When `true` (default), `submit_order`, `modify_order`, `cancel_order`,
215    /// and `submit_order_list` route through the authenticated WebSocket
216    /// connection when active, falling back to REST when the WebSocket is
217    /// inactive. When `false`, all order operations use REST only.
218    #[builder(default = true)]
219    pub use_ws_trade: bool,
220
221    /// Timeout in seconds for WebSocket order responses.
222    ///
223    /// Submit, amend, and batch-add timeouts emit rejection events. Cancel
224    /// timeouts log and await reconciliation.
225    #[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    /// Returns the HTTP base URL for the configured product type and environment.
237    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    /// Returns the WebSocket URL for the configured product type and environment.
244    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    /// Validates config invariants.
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if `default_leverage` is set on a Cash account or the demo environment is
255    /// used for Spot.
256    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}