Skip to main content

nautilus_coinapi/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 CoinAPI configuration.
17
18use 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    /// Native instruments to load from the active symbols of one CoinAPI exchange.
35    ///
36    /// The optional filters bound the symbol metadata response and its credit cost.
37    #[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    /// Configuration for the CoinAPI live data client.
80    ///
81    /// When `api_key` is `None`, the client reads `COINAPI_API_KEY` at construction.
82    #[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}