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,
30};
31
32use crate::{
33    common::{
34        consts::{KRAKEN, KRAKEN_VENUE},
35        enums::KrakenProductType,
36    },
37    config::{KrakenDataClientConfig, KrakenExecClientConfig},
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.core.nautilus_pyo3.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 KrakenExecClientConfig {
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.core.nautilus_pyo3.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        name: &str,
153        config: &dyn ClientConfig,
154        cache: CacheView,
155    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
156        let kraken_config = config
157            .as_any()
158            .downcast_ref::<KrakenExecClientConfig>()
159            .ok_or_else(|| {
160                anyhow::anyhow!(
161                    "Invalid config type for KrakenExecutionClientFactory. Expected KrakenExecClientConfig, was {config:?}",
162                )
163            })?
164            .clone();
165
166        kraken_config.validate()?;
167
168        let oms_type = OmsType::Netting;
169        let account_type = match kraken_config.product_type {
170            KrakenProductType::Spot => kraken_config.spot_account_type,
171            KrakenProductType::Futures => AccountType::Margin,
172        };
173
174        let client_id = ClientId::from(name);
175        let core = ExecutionClientCore::new(
176            kraken_config.trader_id,
177            client_id,
178            *KRAKEN_VENUE,
179            oms_type,
180            kraken_config.account_id,
181            account_type,
182            None, // base_currency
183            cache,
184        );
185
186        match kraken_config.product_type {
187            KrakenProductType::Spot => {
188                let client = KrakenSpotExecutionClient::new(core, kraken_config)?;
189                Ok(Box::new(client))
190            }
191            KrakenProductType::Futures => {
192                let client = KrakenFuturesExecutionClient::new(core, kraken_config)?;
193                Ok(Box::new(client))
194            }
195        }
196    }
197
198    fn name(&self) -> &'static str {
199        KRAKEN
200    }
201
202    fn config_type(&self) -> &'static str {
203        "KrakenExecClientConfig"
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use std::{cell::RefCell, rc::Rc};
210
211    use nautilus_common::{
212        cache::Cache,
213        clock::TestClock,
214        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
215        live::runner::set_data_event_sender,
216        messages::DataEvent,
217    };
218    use rstest::rstest;
219
220    use super::*;
221    use crate::common::enums::{KrakenEnvironment, KrakenProductType};
222
223    fn setup_test_env() {
224        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
225        set_data_event_sender(sender);
226    }
227
228    #[rstest]
229    fn test_kraken_data_client_factory_creation() {
230        let factory = KrakenDataClientFactory::new();
231        assert_eq!(factory.name(), KRAKEN);
232        assert_eq!(factory.config_type(), "KrakenDataClientConfig");
233    }
234
235    #[rstest]
236    fn test_kraken_data_client_factory_default() {
237        let factory = KrakenDataClientFactory::new();
238        assert_eq!(factory.name(), KRAKEN);
239    }
240
241    #[rstest]
242    fn test_kraken_data_client_config_implements_client_config() {
243        let config = KrakenDataClientConfig {
244            product_type: KrakenProductType::Spot,
245            ..Default::default()
246        };
247
248        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
249        let downcasted = boxed_config
250            .as_any()
251            .downcast_ref::<KrakenDataClientConfig>();
252
253        assert!(downcasted.is_some());
254    }
255
256    #[rstest]
257    fn test_kraken_data_client_factory_creates_client() {
258        setup_test_env();
259
260        let factory = KrakenDataClientFactory::new();
261        let config = KrakenDataClientConfig {
262            product_type: KrakenProductType::Spot,
263            ..Default::default()
264        };
265
266        let cache = Rc::new(RefCell::new(Cache::default()));
267        let clock = Rc::new(RefCell::new(TestClock::new()));
268
269        let result = factory.create("KRAKEN-TEST", &config, cache.into(), clock);
270        assert!(result.is_ok());
271
272        let client = result.unwrap();
273        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
274    }
275
276    #[rstest]
277    fn test_kraken_execution_client_factory_creates_spot_client_with_netting_oms() {
278        let factory = KrakenExecutionClientFactory::new();
279        let config = KrakenExecClientConfig {
280            product_type: KrakenProductType::Spot,
281            ..Default::default()
282        };
283        let cache = Rc::new(RefCell::new(Cache::default()));
284
285        let result = factory.create("KRAKEN-TEST", &config, cache.into());
286        assert!(result.is_ok());
287
288        let client = result.unwrap();
289        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
290        assert_eq!(client.account_id(), config.account_id);
291        assert_eq!(client.oms_type(), OmsType::Netting);
292    }
293
294    #[rstest]
295    fn test_kraken_execution_client_factory_creates_futures_client_with_netting_oms() {
296        let factory = KrakenExecutionClientFactory::new();
297        let config = KrakenExecClientConfig {
298            product_type: KrakenProductType::Futures,
299            ..Default::default()
300        };
301        let cache = Rc::new(RefCell::new(Cache::default()));
302
303        let result = factory.create("KRAKEN-TEST", &config, cache.into());
304        assert!(result.is_ok());
305
306        let client = result.unwrap();
307        assert_eq!(client.client_id(), ClientId::from("KRAKEN-TEST"));
308        assert_eq!(client.account_id(), config.account_id);
309        assert_eq!(client.oms_type(), OmsType::Netting);
310    }
311
312    #[rstest]
313    fn test_kraken_execution_client_factory_rejects_leverage_on_cash_account() {
314        let factory = KrakenExecutionClientFactory::new();
315        let config = KrakenExecClientConfig {
316            product_type: KrakenProductType::Spot,
317            spot_account_type: AccountType::Cash,
318            default_leverage: Some(3),
319            ..Default::default()
320        };
321        let cache = Rc::new(RefCell::new(Cache::default()));
322
323        let result = factory.create("KRAKEN-TEST", &config, cache.into());
324        let err = match result {
325            Ok(_) => panic!("expected validation error, factory returned Ok"),
326            Err(e) => e.to_string(),
327        };
328        assert!(
329            err.contains("default_leverage requires spot_account_type=Margin"),
330            "unexpected error: {err}"
331        );
332    }
333
334    #[rstest]
335    fn test_kraken_data_client_factory_rejects_spot_demo() {
336        setup_test_env();
337
338        let factory = KrakenDataClientFactory::new();
339        let config = KrakenDataClientConfig {
340            product_type: KrakenProductType::Spot,
341            environment: KrakenEnvironment::Demo,
342            ..Default::default()
343        };
344
345        let cache = Rc::new(RefCell::new(Cache::default()));
346        let clock = Rc::new(RefCell::new(TestClock::new()));
347
348        let result = factory.create("KRAKEN-TEST", &config, cache.into(), clock);
349        let err = match result {
350            Ok(_) => panic!("expected validation error, factory returned Ok"),
351            Err(e) => e.to_string(),
352        };
353        assert!(
354            err.contains("Kraken Spot does not support the demo environment"),
355            "unexpected error: {err}"
356        );
357    }
358
359    #[rstest]
360    fn test_kraken_execution_client_factory_accepts_leverage_on_margin_account() {
361        let factory = KrakenExecutionClientFactory::new();
362        let config = KrakenExecClientConfig {
363            product_type: KrakenProductType::Spot,
364            spot_account_type: AccountType::Margin,
365            default_leverage: Some(3),
366            ..Default::default()
367        };
368        let cache = Rc::new(RefCell::new(Cache::default()));
369
370        let result = factory.create("KRAKEN-TEST", &config, cache.into());
371        assert!(result.is_ok());
372    }
373}