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