1use 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 #[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 #[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}