use std::collections::HashMap;
use nautilus_common::{config::check_unique_values, python::config_error_to_pyvalue_err};
use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
use nautilus_model::identifiers::InstrumentId;
use nautilus_network::websocket::TransportBackend;
use pyo3::{PyResult, pymethods};
use ustr::Ustr;
use crate::{
config::{CoinApiDataClientConfig, CoinApiInstrumentSelection},
http::query::SymbolsQuery,
};
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl CoinApiInstrumentSelection {
#[new]
#[pyo3(signature = (exchange_id, instrument_ids, filter_symbol_id = None, filter_asset_id = None))]
fn py_new(
exchange_id: &str,
instrument_ids: Vec<InstrumentId>,
filter_symbol_id: Option<String>,
filter_asset_id: Option<String>,
) -> PyResult<Self> {
check_unique_values("instrument_ids", &instrument_ids)
.map_err(config_error_to_pyvalue_err)?;
Ok(Self {
exchange_id: Ustr::from(exchange_id),
query: SymbolsQuery {
filter_symbol_id,
filter_asset_id,
},
instrument_ids,
})
}
#[getter]
fn exchange_id(&self) -> &str {
self.exchange_id.as_str()
}
#[getter]
fn instrument_ids(&self) -> Vec<InstrumentId> {
self.instrument_ids.clone()
}
#[getter]
fn filter_symbol_id(&self) -> Option<String> {
self.query.filter_symbol_id.clone()
}
#[getter]
fn filter_asset_id(&self) -> Option<String> {
self.query.filter_asset_id.clone()
}
}
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl CoinApiDataClientConfig {
#[new]
#[pyo3(signature = (
api_key = None,
base_url_http = None,
base_url_ws = None,
instruments = None,
credit_budget = None,
max_retries = None,
transport_backend = None,
book_snapshot_timeout_secs = None,
price_precisions = None,
proxy_url = None,
http_timeout_secs = None,
max_concurrent_requests = None,
retry_delay_initial_ms = None,
retry_delay_max_ms = None,
retry_max_elapsed_ms = None,
))]
#[expect(clippy::too_many_arguments)]
fn py_new(
api_key: Option<String>,
base_url_http: Option<String>,
base_url_ws: Option<String>,
instruments: Option<Vec<CoinApiInstrumentSelection>>,
credit_budget: Option<u64>,
max_retries: Option<u32>,
transport_backend: Option<TransportBackend>,
book_snapshot_timeout_secs: Option<u64>,
price_precisions: Option<HashMap<InstrumentId, u8>>,
proxy_url: Option<String>,
http_timeout_secs: Option<u64>,
max_concurrent_requests: Option<usize>,
retry_delay_initial_ms: Option<u64>,
retry_delay_max_ms: Option<u64>,
retry_max_elapsed_ms: Option<u64>,
) -> PyResult<Self> {
let defaults = Self::default();
let config = Self {
api_key: api_key.map(SecretString::from),
base_url_http,
base_url_ws,
instruments: instruments.unwrap_or(defaults.instruments),
credit_budget: credit_budget.unwrap_or(defaults.credit_budget),
max_retries: max_retries.unwrap_or(defaults.max_retries),
transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
book_snapshot_timeout_secs: book_snapshot_timeout_secs
.unwrap_or(defaults.book_snapshot_timeout_secs),
price_precisions: price_precisions.unwrap_or(defaults.price_precisions),
proxy_url: proxy_url.map(SecretString::from),
http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
max_concurrent_requests: max_concurrent_requests
.unwrap_or(defaults.max_concurrent_requests),
retry_delay_initial_ms: retry_delay_initial_ms
.unwrap_or(defaults.retry_delay_initial_ms),
retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
retry_max_elapsed_ms: retry_max_elapsed_ms.unwrap_or(defaults.retry_max_elapsed_ms),
};
config.validate().map_err(to_pyvalue_err)?;
Ok(config)
}
#[getter]
const fn has_proxy_url(&self) -> bool {
self.proxy_url.is_some()
}
fn __repr__(&self) -> String {
stringify!(CoinApiDataClientConfig).to_string()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(TransportBackend::Tungstenite)]
#[case(TransportBackend::Sockudo)]
fn constructors_preserve_selection_and_configuration_fields(#[case] backend: TransportBackend) {
let instrument_id = InstrumentId::from("BTC-USD.COINBASE");
let selection = CoinApiInstrumentSelection::py_new(
"COINBASE",
vec![instrument_id],
Some("COINBASE_SPOT_BTC_USD".into()),
Some("BTC".into()),
)
.unwrap();
assert_eq!(selection.exchange_id(), "COINBASE");
assert_eq!(selection.instrument_ids(), [instrument_id]);
assert_eq!(
selection.filter_symbol_id().as_deref(),
Some("COINBASE_SPOT_BTC_USD")
);
assert_eq!(selection.filter_asset_id().as_deref(), Some("BTC"));
let config = CoinApiDataClientConfig::py_new(
Some("test-key".into()),
Some("http://localhost:8123".into()),
Some("ws://localhost:8124".into()),
Some(vec![selection.clone()]),
Some(17),
Some(2),
Some(backend),
Some(7),
Some(HashMap::from([(instrument_id, 3)])),
Some("http://user:proxy-secret@localhost:8125".into()),
Some(13),
Some(4),
Some(29),
Some(53),
Some(71_000),
)
.unwrap();
assert_eq!(config.api_key.as_ref().unwrap().expose_secret(), "test-key");
assert_eq!(
config.base_url_http.as_deref(),
Some("http://localhost:8123")
);
assert_eq!(config.base_url_ws.as_deref(), Some("ws://localhost:8124"));
assert_eq!(config.instruments.len(), 1);
assert_eq!(config.instruments[0].exchange_id, selection.exchange_id);
assert_eq!(
config.instruments[0].instrument_ids,
selection.instrument_ids
);
assert_eq!(
config.instruments[0].query.filter_symbol_id,
selection.query.filter_symbol_id
);
assert_eq!(
config.instruments[0].query.filter_asset_id,
selection.query.filter_asset_id
);
assert_eq!(
config.proxy_url.as_ref().unwrap().expose_secret(),
"http://user:proxy-secret@localhost:8125"
);
assert_eq!(config.http_timeout_secs, 13);
assert_eq!(config.max_concurrent_requests, 4);
assert_eq!(config.retry_delay_initial_ms, 29);
assert_eq!(config.retry_delay_max_ms, 53);
assert_eq!(config.retry_max_elapsed_ms, 71_000);
assert_eq!(config.credit_budget, 17);
assert_eq!(config.max_retries, 2);
assert_eq!(config.transport_backend, backend);
assert_eq!(config.book_snapshot_timeout_secs, 7);
assert_eq!(config.price_precisions, HashMap::from([(instrument_id, 3)]));
}
}