1use std::collections::HashMap;
19
20use nautilus_common::{config::check_unique_values, python::config_error_to_pyvalue_err};
21use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
22use nautilus_model::identifiers::InstrumentId;
23use nautilus_network::websocket::TransportBackend;
24use pyo3::{PyResult, pymethods};
25use ustr::Ustr;
26
27use crate::{
28 config::{CoinApiDataClientConfig, CoinApiInstrumentSelection},
29 http::query::SymbolsQuery,
30};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl CoinApiInstrumentSelection {
35 #[new]
39 #[pyo3(signature = (exchange_id, instrument_ids, filter_symbol_id = None, filter_asset_id = None))]
40 fn py_new(
41 exchange_id: &str,
42 instrument_ids: Vec<InstrumentId>,
43 filter_symbol_id: Option<String>,
44 filter_asset_id: Option<String>,
45 ) -> PyResult<Self> {
46 check_unique_values("instrument_ids", &instrument_ids)
47 .map_err(config_error_to_pyvalue_err)?;
48 Ok(Self {
49 exchange_id: Ustr::from(exchange_id),
50 query: SymbolsQuery {
51 filter_symbol_id,
52 filter_asset_id,
53 },
54 instrument_ids,
55 })
56 }
57
58 #[getter]
59 fn exchange_id(&self) -> &str {
60 self.exchange_id.as_str()
61 }
62
63 #[getter]
64 fn instrument_ids(&self) -> Vec<InstrumentId> {
65 self.instrument_ids.clone()
66 }
67
68 #[getter]
69 fn filter_symbol_id(&self) -> Option<String> {
70 self.query.filter_symbol_id.clone()
71 }
72
73 #[getter]
74 fn filter_asset_id(&self) -> Option<String> {
75 self.query.filter_asset_id.clone()
76 }
77}
78
79#[pymethods]
80#[pyo3_stub_gen::derive::gen_stub_pymethods]
81impl CoinApiDataClientConfig {
82 #[new]
86 #[pyo3(signature = (
87 api_key = None,
88 base_url_http = None,
89 base_url_ws = None,
90 instruments = None,
91 credit_budget = None,
92 max_retries = None,
93 transport_backend = None,
94 book_snapshot_timeout_secs = None,
95 price_precisions = None,
96 proxy_url = None,
97 http_timeout_secs = None,
98 max_concurrent_requests = None,
99 retry_delay_initial_ms = None,
100 retry_delay_max_ms = None,
101 retry_max_elapsed_ms = None,
102 ))]
103 #[expect(clippy::too_many_arguments)]
104 fn py_new(
105 api_key: Option<String>,
106 base_url_http: Option<String>,
107 base_url_ws: Option<String>,
108 instruments: Option<Vec<CoinApiInstrumentSelection>>,
109 credit_budget: Option<u64>,
110 max_retries: Option<u32>,
111 transport_backend: Option<TransportBackend>,
112 book_snapshot_timeout_secs: Option<u64>,
113 price_precisions: Option<HashMap<InstrumentId, u8>>,
114 proxy_url: Option<String>,
115 http_timeout_secs: Option<u64>,
116 max_concurrent_requests: Option<usize>,
117 retry_delay_initial_ms: Option<u64>,
118 retry_delay_max_ms: Option<u64>,
119 retry_max_elapsed_ms: Option<u64>,
120 ) -> PyResult<Self> {
121 let defaults = Self::default();
122
123 let config = Self {
124 api_key: api_key.map(SecretString::from),
125 base_url_http,
126 base_url_ws,
127 instruments: instruments.unwrap_or(defaults.instruments),
128 credit_budget: credit_budget.unwrap_or(defaults.credit_budget),
129 max_retries: max_retries.unwrap_or(defaults.max_retries),
130 transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
131 book_snapshot_timeout_secs: book_snapshot_timeout_secs
132 .unwrap_or(defaults.book_snapshot_timeout_secs),
133 price_precisions: price_precisions.unwrap_or(defaults.price_precisions),
134 proxy_url: proxy_url.map(SecretString::from),
135 http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
136 max_concurrent_requests: max_concurrent_requests
137 .unwrap_or(defaults.max_concurrent_requests),
138 retry_delay_initial_ms: retry_delay_initial_ms
139 .unwrap_or(defaults.retry_delay_initial_ms),
140 retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
141 retry_max_elapsed_ms: retry_max_elapsed_ms.unwrap_or(defaults.retry_max_elapsed_ms),
142 };
143
144 config.validate().map_err(to_pyvalue_err)?;
145 Ok(config)
146 }
147
148 #[getter]
149 const fn has_proxy_url(&self) -> bool {
150 self.proxy_url.is_some()
151 }
152
153 fn __repr__(&self) -> String {
154 stringify!(CoinApiDataClientConfig).to_string()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161
162 use super::*;
163
164 #[rstest]
165 #[case(TransportBackend::Tungstenite)]
166 #[case(TransportBackend::Sockudo)]
167 fn constructors_preserve_selection_and_configuration_fields(#[case] backend: TransportBackend) {
168 let instrument_id = InstrumentId::from("BTC-USD.COINBASE");
169 let selection = CoinApiInstrumentSelection::py_new(
170 "COINBASE",
171 vec![instrument_id],
172 Some("COINBASE_SPOT_BTC_USD".into()),
173 Some("BTC".into()),
174 )
175 .unwrap();
176 assert_eq!(selection.exchange_id(), "COINBASE");
177 assert_eq!(selection.instrument_ids(), [instrument_id]);
178 assert_eq!(
179 selection.filter_symbol_id().as_deref(),
180 Some("COINBASE_SPOT_BTC_USD")
181 );
182 assert_eq!(selection.filter_asset_id().as_deref(), Some("BTC"));
183 let config = CoinApiDataClientConfig::py_new(
184 Some("test-key".into()),
185 Some("http://localhost:8123".into()),
186 Some("ws://localhost:8124".into()),
187 Some(vec![selection.clone()]),
188 Some(17),
189 Some(2),
190 Some(backend),
191 Some(7),
192 Some(HashMap::from([(instrument_id, 3)])),
193 Some("http://user:proxy-secret@localhost:8125".into()),
194 Some(13),
195 Some(4),
196 Some(29),
197 Some(53),
198 Some(71_000),
199 )
200 .unwrap();
201
202 assert_eq!(config.api_key.as_ref().unwrap().expose_secret(), "test-key");
203 assert_eq!(
204 config.base_url_http.as_deref(),
205 Some("http://localhost:8123")
206 );
207 assert_eq!(config.base_url_ws.as_deref(), Some("ws://localhost:8124"));
208 assert_eq!(config.instruments.len(), 1);
209 assert_eq!(config.instruments[0].exchange_id, selection.exchange_id);
210 assert_eq!(
211 config.instruments[0].instrument_ids,
212 selection.instrument_ids
213 );
214 assert_eq!(
215 config.instruments[0].query.filter_symbol_id,
216 selection.query.filter_symbol_id
217 );
218 assert_eq!(
219 config.instruments[0].query.filter_asset_id,
220 selection.query.filter_asset_id
221 );
222 assert_eq!(
223 config.proxy_url.as_ref().unwrap().expose_secret(),
224 "http://user:proxy-secret@localhost:8125"
225 );
226 assert_eq!(config.http_timeout_secs, 13);
227 assert_eq!(config.max_concurrent_requests, 4);
228 assert_eq!(config.retry_delay_initial_ms, 29);
229 assert_eq!(config.retry_delay_max_ms, 53);
230 assert_eq!(config.retry_max_elapsed_ms, 71_000);
231 assert_eq!(config.credit_budget, 17);
232 assert_eq!(config.max_retries, 2);
233 assert_eq!(config.transport_backend, backend);
234 assert_eq!(config.book_snapshot_timeout_secs, 7);
235 assert_eq!(config.price_precisions, HashMap::from([(instrument_id, 3)]));
236 }
237}