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