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