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