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        let proxy_url = dydx_config
122            .proxy_url
123            .as_ref()
124            .map(|value| value.expose_secret().to_owned());
125
126        let http_client = DydxHttpClient::new(
127            Some(http_url),
128            dydx_config.http_timeout_secs,
129            proxy_url.clone(),
130            dydx_config.network,
131            retry_config,
132        )?;
133
134        let ws_client = DydxWebSocketClient::new_public_with_cache_and_pool(
135            ws_url,
136            Arc::new(InstrumentCache::new()),
137            Some(20),
138            dydx_config.transport_backend,
139            proxy_url,
140            dydx_config.max_ws_connections,
141            dydx_config.per_channel_subscription_limit,
142        );
143
144        let client = DydxDataClient::new(client_id, dydx_config, http_client, ws_client)?;
145        Ok(Box::new(client))
146    }
147
148    fn name(&self) -> &'static str {
149        DYDX
150    }
151
152    fn config_type(&self) -> &'static str {
153        "DydxDataClientConfig"
154    }
155}
156
157/// Factory for creating dYdX execution clients.
158#[derive(Debug, Clone)]
159#[cfg_attr(
160    feature = "python",
161    pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
162)]
163#[cfg_attr(
164    feature = "python",
165    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
166)]
167pub struct DydxExecutionClientFactory;
168
169impl DydxExecutionClientFactory {
170    /// Creates a new [`DydxExecutionClientFactory`] instance.
171    #[must_use]
172    pub const fn new() -> Self {
173        Self
174    }
175}
176
177impl Default for DydxExecutionClientFactory {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl ExecutionClientFactory for DydxExecutionClientFactory {
184    fn create(
185        &self,
186        trader_id: TraderId,
187        name: &str,
188        config: &dyn ClientConfig,
189        cache: CacheView,
190        _clock: Rc<RefCell<dyn Clock>>,
191    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
192        let dydx_config = config
193            .as_any()
194            .downcast_ref::<DydxExecutionClientConfig>()
195            .ok_or_else(|| {
196                anyhow::anyhow!(
197                    "Invalid config type for DydxExecutionClientFactory. Expected DydxExecutionClientConfig, was {config:?}",
198                )
199            })?
200            .clone();
201
202        dydx_config.validate()?;
203
204        // dYdX uses netting for perpetual futures
205        let oms_type = OmsType::Netting;
206
207        // dYdX is always margin (perpetual futures)
208        let account_type = AccountType::Margin;
209
210        let core = ExecutionClientCore::new(
211            trader_id,
212            ClientId::from(name),
213            *DYDX_VENUE,
214            oms_type,
215            dydx_config.account_id,
216            account_type,
217            None, // base_currency
218            cache,
219        );
220
221        let adapter_config = DydxAdapterConfig {
222            network: dydx_config.network,
223            base_url: dydx_config.get_http_url(),
224            ws_url: dydx_config.get_ws_url(),
225            grpc_url: dydx_config
226                .get_grpc_urls()
227                .first()
228                .cloned()
229                .unwrap_or_default(),
230            grpc_urls: dydx_config.get_grpc_urls(),
231            chain_id: dydx_config.get_chain_id().to_string(),
232            timeout_secs: dydx_config.http_timeout_secs.unwrap_or(30),
233            wallet_address: dydx_config.wallet_address.clone(),
234            subaccount: dydx_config.subaccount_number,
235            private_key: dydx_config.private_key.clone(),
236            authenticator_ids: dydx_config.authenticator_ids.clone(),
237            max_retries: dydx_config.max_retries.unwrap_or(3),
238            retry_delay_initial_ms: dydx_config.retry_delay_initial_ms.unwrap_or(1000),
239            retry_delay_max_ms: dydx_config.retry_delay_max_ms.unwrap_or(10000),
240            grpc_rate_limit_per_second: dydx_config.grpc_rate_limit_per_second,
241            proxy_url: dydx_config.proxy_url.clone(),
242            transport_backend: dydx_config.transport_backend,
243        };
244
245        log::debug!(
246            "Resolving wallet address: config={:?}, network={}, env_var={}",
247            dydx_config.wallet_address,
248            dydx_config.network,
249            if dydx_config.is_testnet() {
250                "DYDX_TESTNET_WALLET_ADDRESS"
251            } else {
252                "DYDX_WALLET_ADDRESS"
253            }
254        );
255        let wallet_address = if let Some(addr) =
256            resolve_wallet_address(dydx_config.wallet_address.clone(), dydx_config.network)
257        {
258            log::debug!("Using wallet address from config/env: {addr}");
259            addr
260        } else if let Some(credential) = DydxCredential::resolve(
261            dydx_config
262                .private_key
263                .as_ref()
264                .map(|value| value.expose_secret()),
265            dydx_config.network,
266            dydx_config.authenticator_ids.clone(),
267        )? {
268            log::debug!(
269                "Derived wallet address from private key: {}",
270                credential.address
271            );
272            credential.address
273        } else {
274            anyhow::bail!(
275                "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)"
276            )
277        };
278
279        let client = DydxExecutionClient::new(
280            core,
281            adapter_config,
282            wallet_address,
283            dydx_config.subaccount_number,
284        )?;
285
286        Ok(Box::new(client))
287    }
288
289    fn name(&self) -> &'static str {
290        DYDX
291    }
292
293    fn config_type(&self) -> &'static str {
294        "DydxExecutionClientConfig"
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use std::{cell::RefCell, rc::Rc};
301
302    use nautilus_common::{
303        cache::Cache,
304        clock::VirtualClock,
305        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
306    };
307    use nautilus_model::identifiers::{AccountId, TraderId};
308    use rstest::rstest;
309
310    use super::*;
311    use crate::{
312        common::enums::DydxNetwork,
313        config::{DydxDataClientConfig, DydxExecutionClientConfig},
314    };
315
316    #[rstest]
317    fn test_dydx_data_client_factory_creation() {
318        let factory = DydxDataClientFactory::new();
319        assert_eq!(factory.name(), DYDX);
320        assert_eq!(factory.config_type(), "DydxDataClientConfig");
321    }
322
323    #[rstest]
324    fn test_dydx_data_client_factory_default() {
325        let factory = DydxDataClientFactory;
326        assert_eq!(factory.name(), DYDX);
327    }
328
329    #[rstest]
330    fn test_dydx_execution_client_factory_creation() {
331        let factory = DydxExecutionClientFactory::new();
332        assert_eq!(factory.name(), DYDX);
333        assert_eq!(factory.config_type(), "DydxExecutionClientConfig");
334    }
335
336    #[rstest]
337    fn test_dydx_execution_client_factory_default() {
338        let factory = DydxExecutionClientFactory;
339        assert_eq!(factory.name(), DYDX);
340    }
341
342    #[rstest]
343    fn test_dydx_data_client_config_implements_client_config() {
344        let config = DydxDataClientConfig::default();
345        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
346        let downcasted = boxed_config.as_any().downcast_ref::<DydxDataClientConfig>();
347
348        assert!(downcasted.is_some());
349    }
350
351    #[rstest]
352    fn test_dydx_exec_client_config_implements_client_config() {
353        let config = DydxExecutionClientConfig {
354            account_id: AccountId::from("DYDX-001"),
355            network: DydxNetwork::Mainnet,
356            grpc_endpoint: None,
357            grpc_urls: vec![],
358            ws_endpoint: None,
359            http_endpoint: None,
360            private_key: None,
361            wallet_address: Some("dydx1abc123".to_string()),
362            subaccount_number: 0,
363            authenticator_ids: vec![],
364            http_timeout_secs: None,
365            max_retries: None,
366            retry_delay_initial_ms: None,
367            retry_delay_max_ms: None,
368            grpc_rate_limit_per_second: Some(4),
369            proxy_url: None,
370            transport_backend: Default::default(),
371        };
372
373        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
374        let downcasted = boxed_config
375            .as_any()
376            .downcast_ref::<DydxExecutionClientConfig>();
377
378        assert!(downcasted.is_some());
379    }
380
381    #[rstest]
382    fn test_dydx_data_client_factory_rejects_wrong_config_type() {
383        let factory = DydxDataClientFactory::new();
384        let wrong_config = DydxExecutionClientConfig {
385            account_id: AccountId::from("DYDX-001"),
386            network: DydxNetwork::Mainnet,
387            grpc_endpoint: None,
388            grpc_urls: vec![],
389            ws_endpoint: None,
390            http_endpoint: None,
391            private_key: None,
392            wallet_address: None,
393            subaccount_number: 0,
394            authenticator_ids: vec![],
395            http_timeout_secs: None,
396            max_retries: None,
397            retry_delay_initial_ms: None,
398            retry_delay_max_ms: None,
399            grpc_rate_limit_per_second: Some(4),
400            proxy_url: None,
401            transport_backend: Default::default(),
402        };
403
404        let cache = Rc::new(RefCell::new(Cache::default()));
405        let clock = Rc::new(RefCell::new(VirtualClock::new()));
406
407        let result = factory.create("DYDX-TEST", &wrong_config, cache.into(), clock);
408        assert!(result.is_err());
409        assert!(
410            result
411                .err()
412                .unwrap()
413                .to_string()
414                .contains("Invalid config type")
415        );
416    }
417
418    #[rstest]
419    fn test_dydx_execution_client_factory_rejects_wrong_config_type() {
420        let factory = DydxExecutionClientFactory::new();
421        let wrong_config = DydxDataClientConfig::default();
422
423        let cache = Rc::new(RefCell::new(Cache::default()));
424
425        let result = factory.create(
426            TraderId::from("TRADER-001"),
427            "DYDX-TEST",
428            &wrong_config,
429            cache.into(),
430            Rc::new(RefCell::new(VirtualClock::new())),
431        );
432        assert!(result.is_err());
433        assert!(
434            result
435                .err()
436                .unwrap()
437                .to_string()
438                .contains("Invalid config type")
439        );
440    }
441}