Skip to main content

nautilus_dydx/
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 dYdX clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
19
20use log;
21use nautilus_common::{
22    cache::CacheView,
23    clients::{DataClient, ExecutionClient},
24    clock::Clock,
25    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
26};
27use nautilus_live::ExecutionClientCore;
28use nautilus_model::{
29    enums::{AccountType, OmsType},
30    identifiers::ClientId,
31};
32use nautilus_network::retry::RetryConfig;
33
34use crate::{
35    common::{
36        consts::{DYDX, DYDX_VENUE},
37        credential::{DydxCredential, resolve_wallet_address},
38        instrument_cache::InstrumentCache,
39        urls,
40    },
41    config::{DydxAdapterConfig, DydxDataClientConfig, DydxExecClientConfig},
42    data::DydxDataClient,
43    execution::DydxExecutionClient,
44    http::client::DydxHttpClient,
45    websocket::client::DydxWebSocketClient,
46};
47
48impl ClientConfig for DydxDataClientConfig {
49    fn as_any(&self) -> &dyn Any {
50        self
51    }
52}
53
54impl ClientConfig for DydxExecClientConfig {
55    fn as_any(&self) -> &dyn Any {
56        self
57    }
58}
59
60/// Factory for creating dYdX data clients.
61#[derive(Debug, Clone)]
62#[cfg_attr(
63    feature = "python",
64    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
65)]
66#[cfg_attr(
67    feature = "python",
68    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
69)]
70pub struct DydxDataClientFactory;
71
72impl DydxDataClientFactory {
73    /// Creates a new [`DydxDataClientFactory`] instance.
74    #[must_use]
75    pub const fn new() -> Self {
76        Self
77    }
78}
79
80impl Default for DydxDataClientFactory {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl DataClientFactory for DydxDataClientFactory {
87    fn create(
88        &self,
89        name: &str,
90        config: &dyn ClientConfig,
91        _cache: CacheView,
92        _clock: Rc<RefCell<dyn Clock>>,
93    ) -> anyhow::Result<Box<dyn DataClient>> {
94        let dydx_config = config
95            .as_any()
96            .downcast_ref::<DydxDataClientConfig>()
97            .ok_or_else(|| {
98                anyhow::anyhow!(
99                    "Invalid config type for DydxDataClientFactory. Expected DydxDataClientConfig, was {config:?}",
100                )
101            })?
102            .clone();
103
104        let client_id = ClientId::from(name);
105
106        let http_url = dydx_config
107            .base_url_http
108            .clone()
109            .unwrap_or_else(|| urls::http_base_url(dydx_config.network).to_string());
110        let ws_url = dydx_config
111            .base_url_ws
112            .clone()
113            .unwrap_or_else(|| urls::ws_url(dydx_config.network).to_string());
114
115        let retry_config = Some(RetryConfig {
116            max_retries: dydx_config.max_retries as u32,
117            initial_delay_ms: dydx_config.retry_delay_initial_ms,
118            max_delay_ms: dydx_config.retry_delay_max_ms,
119            ..Default::default()
120        });
121
122        let http_client = DydxHttpClient::new(
123            Some(http_url),
124            dydx_config.http_timeout_secs,
125            dydx_config.proxy_url.clone(),
126            dydx_config.network,
127            retry_config,
128        )?;
129
130        let ws_client = DydxWebSocketClient::new_public_with_cache_and_pool(
131            ws_url,
132            Arc::new(InstrumentCache::new()),
133            Some(20),
134            dydx_config.transport_backend,
135            dydx_config.proxy_url.clone(),
136            dydx_config.max_ws_connections,
137            dydx_config.per_channel_subscription_limit,
138        );
139
140        let client = DydxDataClient::new(client_id, dydx_config, http_client, ws_client)?;
141        Ok(Box::new(client))
142    }
143
144    fn name(&self) -> &'static str {
145        DYDX
146    }
147
148    fn config_type(&self) -> &'static str {
149        "DydxDataClientConfig"
150    }
151}
152
153/// Factory for creating dYdX execution clients.
154#[derive(Debug, Clone)]
155#[cfg_attr(
156    feature = "python",
157    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
158)]
159#[cfg_attr(
160    feature = "python",
161    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
162)]
163pub struct DydxExecutionClientFactory;
164
165impl DydxExecutionClientFactory {
166    /// Creates a new [`DydxExecutionClientFactory`] instance.
167    #[must_use]
168    pub const fn new() -> Self {
169        Self
170    }
171}
172
173impl Default for DydxExecutionClientFactory {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl ExecutionClientFactory for DydxExecutionClientFactory {
180    fn create(
181        &self,
182        name: &str,
183        config: &dyn ClientConfig,
184        cache: CacheView,
185    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
186        let dydx_config = config
187            .as_any()
188            .downcast_ref::<DydxExecClientConfig>()
189            .ok_or_else(|| {
190                anyhow::anyhow!(
191                    "Invalid config type for DydxExecutionClientFactory. Expected DydxExecClientConfig, was {config:?}",
192                )
193            })?
194            .clone();
195
196        // dYdX uses netting for perpetual futures
197        let oms_type = OmsType::Netting;
198
199        // dYdX is always margin (perpetual futures)
200        let account_type = AccountType::Margin;
201
202        let core = ExecutionClientCore::new(
203            dydx_config.trader_id,
204            ClientId::from(name),
205            *DYDX_VENUE,
206            oms_type,
207            dydx_config.account_id,
208            account_type,
209            None, // base_currency
210            cache,
211        );
212
213        let adapter_config = DydxAdapterConfig {
214            network: dydx_config.network,
215            base_url: dydx_config.get_http_url(),
216            ws_url: dydx_config.get_ws_url(),
217            grpc_url: dydx_config
218                .get_grpc_urls()
219                .first()
220                .cloned()
221                .unwrap_or_default(),
222            grpc_urls: dydx_config.get_grpc_urls(),
223            chain_id: dydx_config.get_chain_id().to_string(),
224            timeout_secs: dydx_config.http_timeout_secs.unwrap_or(30),
225            wallet_address: dydx_config.wallet_address.clone(),
226            subaccount: dydx_config.subaccount_number,
227            private_key: dydx_config.private_key.clone(),
228            authenticator_ids: dydx_config.authenticator_ids.clone(),
229            max_retries: dydx_config.max_retries.unwrap_or(3),
230            retry_delay_initial_ms: dydx_config.retry_delay_initial_ms.unwrap_or(1000),
231            retry_delay_max_ms: dydx_config.retry_delay_max_ms.unwrap_or(10000),
232            grpc_rate_limit_per_second: dydx_config.grpc_rate_limit_per_second,
233            proxy_url: dydx_config.proxy_url.clone(),
234            transport_backend: dydx_config.transport_backend,
235        };
236
237        log::debug!(
238            "Resolving wallet address: config={:?}, network={}, env_var={}",
239            dydx_config.wallet_address,
240            dydx_config.network,
241            if dydx_config.is_testnet() {
242                "DYDX_TESTNET_WALLET_ADDRESS"
243            } else {
244                "DYDX_WALLET_ADDRESS"
245            }
246        );
247        let wallet_address = if let Some(addr) =
248            resolve_wallet_address(dydx_config.wallet_address.clone(), dydx_config.network)
249        {
250            log::debug!("Using wallet address from config/env: {addr}");
251            addr
252        } else if let Some(credential) = DydxCredential::resolve(
253            dydx_config.private_key.as_deref(),
254            dydx_config.network,
255            dydx_config.authenticator_ids.clone(),
256        )? {
257            log::debug!(
258                "Derived wallet address from private key: {}",
259                credential.address
260            );
261            credential.address
262        } else {
263            anyhow::bail!(
264                "No wallet credentials found: set wallet_address or private_key in config, or use environment variables (DYDX_WALLET_ADDRESS/DYDX_PRIVATE_KEY for mainnet, DYDX_TESTNET_* for testnet)"
265            )
266        };
267
268        let client = DydxExecutionClient::new(
269            core,
270            adapter_config,
271            wallet_address,
272            dydx_config.subaccount_number,
273        )?;
274
275        Ok(Box::new(client))
276    }
277
278    fn name(&self) -> &'static str {
279        DYDX
280    }
281
282    fn config_type(&self) -> &'static str {
283        "DydxExecClientConfig"
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use std::{cell::RefCell, rc::Rc};
290
291    use nautilus_common::{
292        cache::Cache,
293        clock::TestClock,
294        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
295    };
296    use nautilus_model::identifiers::{AccountId, TraderId};
297    use rstest::rstest;
298
299    use super::*;
300    use crate::{
301        common::enums::DydxNetwork,
302        config::{DydxDataClientConfig, DydxExecClientConfig},
303    };
304
305    #[rstest]
306    fn test_dydx_data_client_factory_creation() {
307        let factory = DydxDataClientFactory::new();
308        assert_eq!(factory.name(), DYDX);
309        assert_eq!(factory.config_type(), "DydxDataClientConfig");
310    }
311
312    #[rstest]
313    fn test_dydx_data_client_factory_default() {
314        let factory = DydxDataClientFactory;
315        assert_eq!(factory.name(), DYDX);
316    }
317
318    #[rstest]
319    fn test_dydx_execution_client_factory_creation() {
320        let factory = DydxExecutionClientFactory::new();
321        assert_eq!(factory.name(), DYDX);
322        assert_eq!(factory.config_type(), "DydxExecClientConfig");
323    }
324
325    #[rstest]
326    fn test_dydx_execution_client_factory_default() {
327        let factory = DydxExecutionClientFactory;
328        assert_eq!(factory.name(), DYDX);
329    }
330
331    #[rstest]
332    fn test_dydx_data_client_config_implements_client_config() {
333        let config = DydxDataClientConfig::default();
334        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
335        let downcasted = boxed_config.as_any().downcast_ref::<DydxDataClientConfig>();
336
337        assert!(downcasted.is_some());
338    }
339
340    #[rstest]
341    fn test_dydx_exec_client_config_implements_client_config() {
342        let config = DydxExecClientConfig {
343            trader_id: TraderId::from("TRADER-001"),
344            account_id: AccountId::from("DYDX-001"),
345            network: DydxNetwork::Mainnet,
346            grpc_endpoint: None,
347            grpc_urls: vec![],
348            ws_endpoint: None,
349            http_endpoint: None,
350            private_key: None,
351            wallet_address: Some("dydx1abc123".to_string()),
352            subaccount_number: 0,
353            authenticator_ids: vec![],
354            http_timeout_secs: None,
355            max_retries: None,
356            retry_delay_initial_ms: None,
357            retry_delay_max_ms: None,
358            grpc_rate_limit_per_second: Some(4),
359            proxy_url: None,
360            transport_backend: Default::default(),
361        };
362
363        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
364        let downcasted = boxed_config.as_any().downcast_ref::<DydxExecClientConfig>();
365
366        assert!(downcasted.is_some());
367    }
368
369    #[rstest]
370    fn test_dydx_data_client_factory_rejects_wrong_config_type() {
371        let factory = DydxDataClientFactory::new();
372        let wrong_config = DydxExecClientConfig {
373            trader_id: TraderId::from("TRADER-001"),
374            account_id: AccountId::from("DYDX-001"),
375            network: DydxNetwork::Mainnet,
376            grpc_endpoint: None,
377            grpc_urls: vec![],
378            ws_endpoint: None,
379            http_endpoint: None,
380            private_key: None,
381            wallet_address: None,
382            subaccount_number: 0,
383            authenticator_ids: vec![],
384            http_timeout_secs: None,
385            max_retries: None,
386            retry_delay_initial_ms: None,
387            retry_delay_max_ms: None,
388            grpc_rate_limit_per_second: Some(4),
389            proxy_url: None,
390            transport_backend: Default::default(),
391        };
392
393        let cache = Rc::new(RefCell::new(Cache::default()));
394        let clock = Rc::new(RefCell::new(TestClock::new()));
395
396        let result = factory.create("DYDX-TEST", &wrong_config, cache.into(), clock);
397        assert!(result.is_err());
398        assert!(
399            result
400                .err()
401                .unwrap()
402                .to_string()
403                .contains("Invalid config type")
404        );
405    }
406
407    #[rstest]
408    fn test_dydx_execution_client_factory_rejects_wrong_config_type() {
409        let factory = DydxExecutionClientFactory::new();
410        let wrong_config = DydxDataClientConfig::default();
411
412        let cache = Rc::new(RefCell::new(Cache::default()));
413
414        let result = factory.create("DYDX-TEST", &wrong_config, cache.into());
415        assert!(result.is_err());
416        assert!(
417            result
418                .err()
419                .unwrap()
420                .to_string()
421                .contains("Invalid config type")
422        );
423    }
424}