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, TraderId},
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, DydxExecutionClientConfig},
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 DydxExecutionClientConfig {
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.adapters.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.adapters.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        trader_id: TraderId,
183        name: &str,
184        config: &dyn ClientConfig,
185        cache: CacheView,
186    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
187        let dydx_config = config
188            .as_any()
189            .downcast_ref::<DydxExecutionClientConfig>()
190            .ok_or_else(|| {
191                anyhow::anyhow!(
192                    "Invalid config type for DydxExecutionClientFactory. Expected DydxExecutionClientConfig, was {config:?}",
193                )
194            })?
195            .clone();
196
197        // dYdX uses netting for perpetual futures
198        let oms_type = OmsType::Netting;
199
200        // dYdX is always margin (perpetual futures)
201        let account_type = AccountType::Margin;
202
203        let core = ExecutionClientCore::new(
204            trader_id,
205            ClientId::from(name),
206            *DYDX_VENUE,
207            oms_type,
208            dydx_config.account_id,
209            account_type,
210            None, // base_currency
211            cache,
212        );
213
214        let adapter_config = DydxAdapterConfig {
215            network: dydx_config.network,
216            base_url: dydx_config.get_http_url(),
217            ws_url: dydx_config.get_ws_url(),
218            grpc_url: dydx_config
219                .get_grpc_urls()
220                .first()
221                .cloned()
222                .unwrap_or_default(),
223            grpc_urls: dydx_config.get_grpc_urls(),
224            chain_id: dydx_config.get_chain_id().to_string(),
225            timeout_secs: dydx_config.http_timeout_secs.unwrap_or(30),
226            wallet_address: dydx_config.wallet_address.clone(),
227            subaccount: dydx_config.subaccount_number,
228            private_key: dydx_config.private_key.clone(),
229            authenticator_ids: dydx_config.authenticator_ids.clone(),
230            max_retries: dydx_config.max_retries.unwrap_or(3),
231            retry_delay_initial_ms: dydx_config.retry_delay_initial_ms.unwrap_or(1000),
232            retry_delay_max_ms: dydx_config.retry_delay_max_ms.unwrap_or(10000),
233            grpc_rate_limit_per_second: dydx_config.grpc_rate_limit_per_second,
234            proxy_url: dydx_config.proxy_url.clone(),
235            transport_backend: dydx_config.transport_backend,
236        };
237
238        log::debug!(
239            "Resolving wallet address: config={:?}, network={}, env_var={}",
240            dydx_config.wallet_address,
241            dydx_config.network,
242            if dydx_config.is_testnet() {
243                "DYDX_TESTNET_WALLET_ADDRESS"
244            } else {
245                "DYDX_WALLET_ADDRESS"
246            }
247        );
248        let wallet_address = if let Some(addr) =
249            resolve_wallet_address(dydx_config.wallet_address.clone(), dydx_config.network)
250        {
251            log::debug!("Using wallet address from config/env: {addr}");
252            addr
253        } else if let Some(credential) = DydxCredential::resolve(
254            dydx_config.private_key.as_deref(),
255            dydx_config.network,
256            dydx_config.authenticator_ids.clone(),
257        )? {
258            log::debug!(
259                "Derived wallet address from private key: {}",
260                credential.address
261            );
262            credential.address
263        } else {
264            anyhow::bail!(
265                "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)"
266            )
267        };
268
269        let client = DydxExecutionClient::new(
270            core,
271            adapter_config,
272            wallet_address,
273            dydx_config.subaccount_number,
274        )?;
275
276        Ok(Box::new(client))
277    }
278
279    fn name(&self) -> &'static str {
280        DYDX
281    }
282
283    fn config_type(&self) -> &'static str {
284        "DydxExecutionClientConfig"
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use std::{cell::RefCell, rc::Rc};
291
292    use nautilus_common::{
293        cache::Cache,
294        clock::TestClock,
295        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
296    };
297    use nautilus_model::identifiers::{AccountId, TraderId};
298    use rstest::rstest;
299
300    use super::*;
301    use crate::{
302        common::enums::DydxNetwork,
303        config::{DydxDataClientConfig, DydxExecutionClientConfig},
304    };
305
306    #[rstest]
307    fn test_dydx_data_client_factory_creation() {
308        let factory = DydxDataClientFactory::new();
309        assert_eq!(factory.name(), DYDX);
310        assert_eq!(factory.config_type(), "DydxDataClientConfig");
311    }
312
313    #[rstest]
314    fn test_dydx_data_client_factory_default() {
315        let factory = DydxDataClientFactory;
316        assert_eq!(factory.name(), DYDX);
317    }
318
319    #[rstest]
320    fn test_dydx_execution_client_factory_creation() {
321        let factory = DydxExecutionClientFactory::new();
322        assert_eq!(factory.name(), DYDX);
323        assert_eq!(factory.config_type(), "DydxExecutionClientConfig");
324    }
325
326    #[rstest]
327    fn test_dydx_execution_client_factory_default() {
328        let factory = DydxExecutionClientFactory;
329        assert_eq!(factory.name(), DYDX);
330    }
331
332    #[rstest]
333    fn test_dydx_data_client_config_implements_client_config() {
334        let config = DydxDataClientConfig::default();
335        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
336        let downcasted = boxed_config.as_any().downcast_ref::<DydxDataClientConfig>();
337
338        assert!(downcasted.is_some());
339    }
340
341    #[rstest]
342    fn test_dydx_exec_client_config_implements_client_config() {
343        let config = DydxExecutionClientConfig {
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
365            .as_any()
366            .downcast_ref::<DydxExecutionClientConfig>();
367
368        assert!(downcasted.is_some());
369    }
370
371    #[rstest]
372    fn test_dydx_data_client_factory_rejects_wrong_config_type() {
373        let factory = DydxDataClientFactory::new();
374        let wrong_config = DydxExecutionClientConfig {
375            account_id: AccountId::from("DYDX-001"),
376            network: DydxNetwork::Mainnet,
377            grpc_endpoint: None,
378            grpc_urls: vec![],
379            ws_endpoint: None,
380            http_endpoint: None,
381            private_key: None,
382            wallet_address: None,
383            subaccount_number: 0,
384            authenticator_ids: vec![],
385            http_timeout_secs: None,
386            max_retries: None,
387            retry_delay_initial_ms: None,
388            retry_delay_max_ms: None,
389            grpc_rate_limit_per_second: Some(4),
390            proxy_url: None,
391            transport_backend: Default::default(),
392        };
393
394        let cache = Rc::new(RefCell::new(Cache::default()));
395        let clock = Rc::new(RefCell::new(TestClock::new()));
396
397        let result = factory.create("DYDX-TEST", &wrong_config, cache.into(), clock);
398        assert!(result.is_err());
399        assert!(
400            result
401                .err()
402                .unwrap()
403                .to_string()
404                .contains("Invalid config type")
405        );
406    }
407
408    #[rstest]
409    fn test_dydx_execution_client_factory_rejects_wrong_config_type() {
410        let factory = DydxExecutionClientFactory::new();
411        let wrong_config = DydxDataClientConfig::default();
412
413        let cache = Rc::new(RefCell::new(Cache::default()));
414
415        let result = factory.create(
416            TraderId::from("TRADER-001"),
417            "DYDX-TEST",
418            &wrong_config,
419            cache.into(),
420        );
421        assert!(result.is_err());
422        assert!(
423            result
424                .err()
425                .unwrap()
426                .to_string()
427                .contains("Invalid config type")
428        );
429    }
430}