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