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