Skip to main content

nautilus_kraken/
factories.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//! Factory functions for creating Kraken clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21    cache::CacheView,
22    clients::{DataClient, ExecutionClient},
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25};
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::{
28    enums::{AccountType, OmsType},
29    identifiers::{ClientId, TraderId},
30};
31
32use crate::{
33    common::{
34        consts::{KRAKEN, KRAKEN_VENUE},
35        enums::KrakenProductType,
36    },
37    config::{KrakenDataClientConfig, KrakenExecutionClientConfig},
38    data::{KrakenFuturesDataClient, KrakenSpotDataClient},
39    execution::{KrakenFuturesExecutionClient, KrakenSpotExecutionClient},
40};
41
42impl ClientConfig for KrakenDataClientConfig {
43    fn as_any(&self) -> &dyn Any {
44        self
45    }
46}
47
48/// Factory for creating Kraken data clients.
49#[derive(Debug, Clone)]
50#[cfg_attr(
51    feature = "python",
52    pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
53)]
54#[cfg_attr(
55    feature = "python",
56    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
57)]
58pub struct KrakenDataClientFactory;
59
60impl KrakenDataClientFactory {
61    /// Creates a new [`KrakenDataClientFactory`] instance.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self
65    }
66}
67
68impl Default for KrakenDataClientFactory {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl DataClientFactory for KrakenDataClientFactory {
75    fn create(
76        &self,
77        name: &str,
78        config: &dyn ClientConfig,
79        _cache: CacheView,
80        _clock: Rc<RefCell<dyn Clock>>,
81    ) -> anyhow::Result<Box<dyn DataClient>> {
82        let kraken_config = config
83            .as_any()
84            .downcast_ref::<KrakenDataClientConfig>()
85            .ok_or_else(|| {
86                anyhow::anyhow!(
87                    "Invalid config type for KrakenDataClientFactory. Expected KrakenDataClientConfig, was {config:?}",
88                )
89            })?
90            .clone();
91
92        kraken_config.validate()?;
93
94        let client_id = ClientId::from(name);
95
96        match kraken_config.product_type {
97            KrakenProductType::Spot => {
98                let client = KrakenSpotDataClient::new(client_id, kraken_config)?;
99                Ok(Box::new(client))
100            }
101            KrakenProductType::Futures => {
102                let client = KrakenFuturesDataClient::new(client_id, kraken_config)?;
103                Ok(Box::new(client))
104            }
105        }
106    }
107
108    fn name(&self) -> &'static str {
109        KRAKEN
110    }
111
112    fn config_type(&self) -> &'static str {
113        "KrakenDataClientConfig"
114    }
115}
116
117impl ClientConfig for KrakenExecutionClientConfig {
118    fn as_any(&self) -> &dyn Any {
119        self
120    }
121}
122
123/// Factory for creating Kraken execution clients.
124#[derive(Debug, Clone)]
125#[cfg_attr(
126    feature = "python",
127    pyo3::pyclass(module = "nautilus_trader.adapters.kraken", from_py_object)
128)]
129#[cfg_attr(
130    feature = "python",
131    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
132)]
133pub struct KrakenExecutionClientFactory;
134
135impl KrakenExecutionClientFactory {
136    /// Creates a new [`KrakenExecutionClientFactory`] instance.
137    #[must_use]
138    pub const fn new() -> Self {
139        Self
140    }
141}
142
143impl Default for KrakenExecutionClientFactory {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl ExecutionClientFactory for KrakenExecutionClientFactory {
150    fn create(
151        &self,
152        trader_id: TraderId,
153        name: &str,
154        config: &dyn ClientConfig,
155        cache: CacheView,
156    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
157        let kraken_config = config
158            .as_any()
159            .downcast_ref::<KrakenExecutionClientConfig>()
160            .ok_or_else(|| {
161                anyhow::anyhow!(
162                    "Invalid config type for KrakenExecutionClientFactory. Expected KrakenExecutionClientConfig, was {config:?}",
163                )
164            })?
165            .clone();
166
167        kraken_config.validate()?;
168
169        let oms_type = OmsType::Netting;
170        let account_type = match kraken_config.product_type {
171            KrakenProductType::Spot => kraken_config.spot_account_type,
172            KrakenProductType::Futures => AccountType::Margin,
173        };
174
175        let client_id = ClientId::from(name);
176        let core = ExecutionClientCore::new(
177            trader_id,
178            client_id,
179            *KRAKEN_VENUE,
180            oms_type,
181            kraken_config.account_id,
182            account_type,
183            None, // base_currency
184            cache,
185        );
186
187        match kraken_config.product_type {
188            KrakenProductType::Spot => {
189                let client = KrakenSpotExecutionClient::new(core, kraken_config)?;
190                Ok(Box::new(client))
191            }
192            KrakenProductType::Futures => {
193                let client = KrakenFuturesExecutionClient::new(core, kraken_config)?;
194                Ok(Box::new(client))
195            }
196        }
197    }
198
199    fn name(&self) -> &'static str {
200        KRAKEN
201    }
202
203    fn config_type(&self) -> &'static str {
204        "KrakenExecutionClientConfig"
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use std::{cell::RefCell, rc::Rc};
211
212    use nautilus_common::{
213        cache::Cache,
214        clock::TestClock,
215        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
216        live::runner::set_data_event_sender,
217        messages::DataEvent,
218    };
219    use rstest::rstest;
220
221    use super::*;
222    use crate::common::enums::{KrakenEnvironment, KrakenProductType};
223
224    fn setup_test_env() {
225        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
226        set_data_event_sender(sender);
227    }
228
229    #[rstest]
230    fn test_kraken_data_client_factory_creation() {
231        let factory = KrakenDataClientFactory::new();
232        assert_eq!(factory.name(), KRAKEN);
233        assert_eq!(factory.config_type(), "KrakenDataClientConfig");
234    }
235
236    #[rstest]
237    fn test_kraken_data_client_factory_default() {
238        let factory = KrakenDataClientFactory::new();
239        assert_eq!(factory.name(), KRAKEN);
240    }
241
242    #[rstest]
243    fn test_kraken_data_client_config_implements_client_config() {
244        let config = KrakenDataClientConfig {
245            product_type: KrakenProductType::Spot,
246            ..Default::default()
247        };
248
249        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
250        let downcasted = boxed_config
251            .as_any()
252            .downcast_ref::<KrakenDataClientConfig>();
253
254        assert!(downcasted.is_some());
255    }
256
257    #[rstest]
258    fn test_kraken_data_client_factory_creates_client() {
259        setup_test_env();
260
261        let factory = KrakenDataClientFactory::new();
262        let config = KrakenDataClientConfig {
263            product_type: KrakenProductType::Spot,
264            ..Default::default()
265        };
266
267        let cache = Rc::new(RefCell::new(Cache::default()));
268        let clock = Rc::new(RefCell::new(TestClock::new()));
269
270        let result = factory.create("KRAKEN-TEST", &config, cache.into(), clock);
271        assert!(result.is_ok());
272
273        let client = result.unwrap();
274        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
275    }
276
277    #[rstest]
278    fn test_kraken_execution_client_factory_creates_spot_client_with_netting_oms() {
279        let factory = KrakenExecutionClientFactory::new();
280        let config = KrakenExecutionClientConfig {
281            product_type: KrakenProductType::Spot,
282            ..Default::default()
283        };
284        let cache = Rc::new(RefCell::new(Cache::default()));
285
286        let result = factory.create(
287            TraderId::from("TRADER-001"),
288            "KRAKEN-TEST",
289            &config,
290            cache.into(),
291        );
292        assert!(result.is_ok());
293
294        let client = result.unwrap();
295        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
296        assert_eq!(client.account_id(), config.account_id);
297        assert_eq!(client.oms_type(), OmsType::Netting);
298    }
299
300    #[rstest]
301    fn test_kraken_execution_client_factory_creates_futures_client_with_netting_oms() {
302        let factory = KrakenExecutionClientFactory::new();
303        let config = KrakenExecutionClientConfig {
304            product_type: KrakenProductType::Futures,
305            ..Default::default()
306        };
307        let cache = Rc::new(RefCell::new(Cache::default()));
308
309        let result = factory.create(
310            TraderId::from("TRADER-001"),
311            "KRAKEN-TEST",
312            &config,
313            cache.into(),
314        );
315        assert!(result.is_ok());
316
317        let client = result.unwrap();
318        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
319        assert_eq!(client.account_id(), config.account_id);
320        assert_eq!(client.oms_type(), OmsType::Netting);
321    }
322
323    #[rstest]
324    fn test_kraken_execution_client_factory_rejects_leverage_on_cash_account() {
325        let factory = KrakenExecutionClientFactory::new();
326        let config = KrakenExecutionClientConfig {
327            product_type: KrakenProductType::Spot,
328            spot_account_type: AccountType::Cash,
329            default_leverage: Some(3),
330            ..Default::default()
331        };
332        let cache = Rc::new(RefCell::new(Cache::default()));
333
334        let result = factory.create(
335            TraderId::from("TRADER-001"),
336            "KRAKEN-TEST",
337            &config,
338            cache.into(),
339        );
340        let err = match result {
341            Ok(_) => panic!("expected validation error, factory returned Ok"),
342            Err(e) => e.to_string(),
343        };
344        assert!(
345            err.contains("default_leverage requires spot_account_type=Margin"),
346            "unexpected error: {err}"
347        );
348    }
349
350    #[rstest]
351    fn test_kraken_data_client_factory_rejects_spot_demo() {
352        setup_test_env();
353
354        let factory = KrakenDataClientFactory::new();
355        let config = KrakenDataClientConfig {
356            product_type: KrakenProductType::Spot,
357            environment: KrakenEnvironment::Demo,
358            ..Default::default()
359        };
360
361        let cache = Rc::new(RefCell::new(Cache::default()));
362        let clock = Rc::new(RefCell::new(TestClock::new()));
363
364        let result = factory.create("KRAKEN-TEST", &config, cache.into(), clock);
365        let err = match result {
366            Ok(_) => panic!("expected validation error, factory returned Ok"),
367            Err(e) => e.to_string(),
368        };
369        assert!(
370            err.contains("Kraken Spot does not support the demo environment"),
371            "unexpected error: {err}"
372        );
373    }
374
375    #[rstest]
376    fn test_kraken_execution_client_factory_accepts_leverage_on_margin_account() {
377        let factory = KrakenExecutionClientFactory::new();
378        let config = KrakenExecutionClientConfig {
379            product_type: KrakenProductType::Spot,
380            spot_account_type: AccountType::Margin,
381            default_leverage: Some(3),
382            ..Default::default()
383        };
384        let cache = Rc::new(RefCell::new(Cache::default()));
385
386        let result = factory.create(
387            TraderId::from("TRADER-001"),
388            "KRAKEN-TEST",
389            &config,
390            cache.into(),
391        );
392        assert!(result.is_ok());
393    }
394}