Skip to main content

nautilus_derive/python/
config.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//! Python bindings for Derive configuration.
17
18use nautilus_common::python::config_error_to_pyvalue_err;
19use nautilus_core::string::secret::SecretString;
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use pyo3::{PyResult, pymethods};
23use rust_decimal::Decimal;
24
25use crate::{
26    common::enums::DeriveEnvironment,
27    config::{DeriveDataClientConfig, DeriveExecutionClientConfig},
28};
29
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl DeriveDataClientConfig {
33    /// Configuration for the Derive data client.
34    #[new]
35    #[pyo3(signature = (
36        base_url_rest = None,
37        base_url_ws = None,
38        proxy_url = None,
39        environment = None,
40        http_timeout_secs = None,
41        ws_timeout_secs = None,
42        update_instruments_interval_mins = None,
43        currencies = None,
44        include_expired = None,
45        auto_load_missing_instruments = None,
46        transport_backend = None,
47        book_snapshot_timeout_secs = None,
48        book_stale_threshold_secs = None,
49    ))]
50    #[expect(clippy::too_many_arguments)]
51    fn py_new(
52        base_url_rest: Option<String>,
53        base_url_ws: Option<String>,
54        proxy_url: Option<String>,
55        environment: Option<DeriveEnvironment>,
56        http_timeout_secs: Option<u64>,
57        ws_timeout_secs: Option<u64>,
58        update_instruments_interval_mins: Option<u64>,
59        currencies: Option<Vec<String>>,
60        include_expired: Option<bool>,
61        auto_load_missing_instruments: Option<bool>,
62        transport_backend: Option<TransportBackend>,
63        book_snapshot_timeout_secs: Option<u64>,
64        book_stale_threshold_secs: Option<u64>,
65    ) -> PyResult<Self> {
66        let defaults = Self::default();
67
68        let config = Self {
69            base_url_rest,
70            base_url_ws,
71            proxy_url: proxy_url.map(SecretString::from),
72            environment: environment.unwrap_or(defaults.environment),
73            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
74            ws_timeout_secs,
75            book_snapshot_timeout_secs: book_snapshot_timeout_secs
76                .unwrap_or(defaults.book_snapshot_timeout_secs),
77            book_stale_threshold_secs: book_stale_threshold_secs
78                .unwrap_or(defaults.book_stale_threshold_secs),
79            update_instruments_interval_mins: update_instruments_interval_mins
80                .unwrap_or(defaults.update_instruments_interval_mins),
81            currencies: currencies.unwrap_or(defaults.currencies),
82            include_expired: include_expired.unwrap_or(defaults.include_expired),
83            auto_load_missing_instruments: auto_load_missing_instruments
84                .unwrap_or(defaults.auto_load_missing_instruments),
85            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
86        };
87
88        config.validate().map_err(config_error_to_pyvalue_err)?;
89        Ok(config)
90    }
91
92    #[getter]
93    const fn has_proxy_url(&self) -> bool {
94        self.proxy_url.is_some()
95    }
96
97    fn __repr__(&self) -> String {
98        stringify!(DeriveDataClientConfig).to_string()
99    }
100}
101
102#[pymethods]
103#[pyo3_stub_gen::derive::gen_stub_pymethods]
104impl DeriveExecutionClientConfig {
105    /// Configuration for the Derive execution client.
106    #[new]
107    #[pyo3(signature = (
108        account_id = None,
109        wallet_address = None,
110        session_key = None,
111        subaccount_id = None,
112        base_url_rest = None,
113        base_url_ws = None,
114        proxy_url = None,
115        environment = None,
116        http_timeout_secs = None,
117        max_retries = None,
118        retry_delay_initial_ms = None,
119        retry_delay_max_ms = None,
120        ws_timeout_secs = None,
121        max_fee_per_contract = None,
122        domain_separator = None,
123        action_typehash = None,
124        trade_module_address = None,
125        signature_expiry_secs = None,
126        market_order_slippage_bps = None,
127        max_matching_requests_per_second = None,
128        max_per_instrument_matching_requests_per_second = None,
129        transport_backend = None,
130        cancel_on_disconnect = None,
131    ))]
132    #[expect(clippy::too_many_arguments)]
133    fn py_new(
134        account_id: Option<AccountId>,
135        wallet_address: Option<String>,
136        session_key: Option<String>,
137        subaccount_id: Option<u64>,
138        base_url_rest: Option<String>,
139        base_url_ws: Option<String>,
140        proxy_url: Option<String>,
141        environment: Option<DeriveEnvironment>,
142        http_timeout_secs: Option<u64>,
143        max_retries: Option<u32>,
144        retry_delay_initial_ms: Option<u64>,
145        retry_delay_max_ms: Option<u64>,
146        ws_timeout_secs: Option<u64>,
147        max_fee_per_contract: Option<Decimal>,
148        domain_separator: Option<String>,
149        action_typehash: Option<String>,
150        trade_module_address: Option<String>,
151        signature_expiry_secs: Option<u64>,
152        market_order_slippage_bps: Option<u32>,
153        max_matching_requests_per_second: Option<u32>,
154        max_per_instrument_matching_requests_per_second: Option<u32>,
155        transport_backend: Option<TransportBackend>,
156        cancel_on_disconnect: Option<bool>,
157    ) -> Self {
158        let defaults = Self::default();
159
160        Self {
161            account_id: account_id.unwrap_or(defaults.account_id),
162            wallet_address,
163            session_key: session_key.map(SecretString::from),
164            subaccount_id,
165            base_url_rest,
166            base_url_ws,
167            proxy_url: proxy_url.map(SecretString::from),
168            environment: environment.unwrap_or(defaults.environment),
169            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
170            max_retries: max_retries.unwrap_or(defaults.max_retries),
171            retry_delay_initial_ms: retry_delay_initial_ms
172                .unwrap_or(defaults.retry_delay_initial_ms),
173            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
174            ws_timeout_secs,
175            max_fee_per_contract,
176            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
177            domain_separator,
178            action_typehash,
179            trade_module_address,
180            signature_expiry_secs: signature_expiry_secs.unwrap_or(defaults.signature_expiry_secs),
181            market_order_slippage_bps: market_order_slippage_bps
182                .unwrap_or(defaults.market_order_slippage_bps),
183            max_matching_requests_per_second,
184            max_per_instrument_matching_requests_per_second,
185            cancel_on_disconnect: cancel_on_disconnect.unwrap_or(defaults.cancel_on_disconnect),
186        }
187    }
188
189    #[getter]
190    const fn has_proxy_url(&self) -> bool {
191        self.proxy_url.is_some()
192    }
193
194    fn __repr__(&self) -> String {
195        stringify!(DeriveExecutionClientConfig).to_string()
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use pyo3::{prelude::*, types::PyDict};
202    use rstest::rstest;
203
204    use super::*;
205
206    #[rstest]
207    #[case::default(None, false)]
208    #[case::disabled(Some(false), false)]
209    #[case::enabled(Some(true), true)]
210    fn test_python_cancel_on_disconnect(#[case] enabled: Option<bool>, #[case] expected: bool) {
211        Python::initialize();
212        Python::attach(|py| {
213            let kwargs = PyDict::new(py);
214            if let Some(enabled) = enabled {
215                kwargs.set_item("cancel_on_disconnect", enabled).unwrap();
216            }
217
218            let config = py
219                .get_type::<DeriveExecutionClientConfig>()
220                .call((), Some(&kwargs))
221                .unwrap();
222            let getter = config
223                .getattr("cancel_on_disconnect")
224                .unwrap()
225                .extract::<bool>()
226                .unwrap();
227            let rust_config = config
228                .extract::<PyRef<DeriveExecutionClientConfig>>()
229                .unwrap();
230
231            assert_eq!(getter, expected);
232            assert_eq!(rust_config.cancel_on_disconnect, expected);
233        });
234    }
235
236    #[rstest]
237    fn test_python_currencies_normalized_uniqueness(#[values(false, true)] duplicate: bool) {
238        Python::initialize();
239        Python::attach(|py| {
240            let mut values = vec![" ETH ".to_string(), "BTC".to_string()];
241            if duplicate {
242                values.push("ETH".to_string());
243            }
244
245            let kwargs = PyDict::new(py);
246            kwargs.set_item("currencies", &values).unwrap();
247            let result = py
248                .get_type::<DeriveDataClientConfig>()
249                .call((), Some(&kwargs));
250
251            if duplicate {
252                let error = result.unwrap_err();
253                assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
254                assert_eq!(
255                    error.value(py).str().unwrap().to_str().unwrap(),
256                    "duplicate currencies"
257                );
258            } else {
259                let config = result.unwrap();
260                let config = config.extract::<PyRef<DeriveDataClientConfig>>().unwrap();
261                assert_eq!(config.currencies, values);
262            }
263        });
264    }
265}