1use std::{collections::HashMap, fmt::Debug, sync::Arc};
17
18use ahash::{AHashMap, AHashSet};
19use nautilus_core::{
20 UnixNanos,
21 consts::NAUTILUS_USER_AGENT,
22 string::{parsing::precision_from_str, secret::REDACTED, urlencoding},
23};
24use nautilus_model::instruments::InstrumentAny;
25use nautilus_network::http::{HttpClient, USER_AGENT};
26
27use super::{
28 error::{Error, TardisErrorResponse},
29 instruments::is_available,
30 models::TardisInstrumentInfo,
31 parse::parse_instrument_any,
32 query::InstrumentFilter,
33};
34use crate::{
35 common::{
36 consts::{TARDIS_REST_QUOTA, TARDIS_REST_RATE_KEY},
37 credential::Credential,
38 enums::TardisExchange,
39 parse::{normalize_instrument_id, parse_instrument_id},
40 urls::TARDIS_HTTP_BASE_URL,
41 },
42 machine::types::{TardisInstrumentKey, TardisInstrumentMiniInfo},
43};
44
45pub type Result<T> = std::result::Result<T, Error>;
46
47#[cfg_attr(
50 feature = "python",
51 pyo3::pyclass(module = "nautilus_trader.adapters.tardis", from_py_object)
52)]
53#[cfg_attr(
54 feature = "python",
55 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.tardis")
56)]
57#[derive(Clone)]
58pub struct TardisHttpClient {
59 base_url: String,
60 credential: Option<Credential>,
61 client: HttpClient,
62 normalize_symbols: bool,
63}
64
65impl Debug for TardisHttpClient {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct(stringify!(TardisHttpClient))
68 .field("base_url", &self.base_url)
69 .field("credential", &self.credential.as_ref().map(|_| REDACTED))
70 .field("normalize_symbols", &self.normalize_symbols)
71 .finish()
72 }
73}
74
75impl TardisHttpClient {
76 pub fn new(
83 api_key: Option<&str>,
84 base_url: Option<&str>,
85 timeout_secs: Option<u64>,
86 normalize_symbols: bool,
87 proxy_url: Option<String>,
88 ) -> anyhow::Result<Self> {
89 let credential = Credential::resolve(api_key.map(ToString::to_string));
90
91 if credential.is_none() {
92 anyhow::bail!(
93 "API key must be provided or set in the 'TARDIS_API_KEY' environment variable"
94 );
95 }
96
97 let base_url =
98 base_url.map_or_else(|| TARDIS_HTTP_BASE_URL.to_string(), ToString::to_string);
99
100 let mut headers = HashMap::new();
101 headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
102
103 if let Some(ref cred) = credential {
104 headers.insert(
105 "Authorization".to_string(),
106 format!("Bearer {}", cred.api_key()),
107 );
108 }
109
110 let keyed_quotas = vec![(TARDIS_REST_RATE_KEY.to_string(), *TARDIS_REST_QUOTA)];
111 let client = HttpClient::builder()
112 .headers(headers)
113 .keyed_quotas(keyed_quotas)
114 .default_quota(*TARDIS_REST_QUOTA)
115 .maybe_timeout_secs(timeout_secs.or(Some(60)))
116 .maybe_proxy_url(proxy_url)
117 .build()?;
118
119 Ok(Self {
120 base_url,
121 credential,
122 client,
123 normalize_symbols,
124 })
125 }
126
127 #[must_use]
129 pub const fn credential(&self) -> Option<&Credential> {
130 self.credential.as_ref()
131 }
132
133 pub async fn instruments_info(
141 &self,
142 exchange: TardisExchange,
143 symbol: Option<&str>,
144 filter: Option<&InstrumentFilter>,
145 ) -> Result<Vec<TardisInstrumentInfo>> {
146 let mut url = format!("{}/instruments/{exchange}", self.base_url);
147
148 if let Some(symbol) = symbol {
149 url.push_str(&format!("/{symbol}"));
150 }
151
152 if let Some(filter) = filter
153 && let Ok(filter_json) = serde_json::to_string(filter)
154 {
155 url.push_str(&format!("?filter={}", urlencoding::encode(&filter_json)));
156 }
157 log::debug!("Requesting: {url}");
158
159 let rate_keys = Some(vec![TARDIS_REST_RATE_KEY.to_string()]);
160 let response = self
161 .client
162 .get(url, None, None, None, rate_keys)
163 .await
164 .map_err(|e| Error::Request(e.to_string()))?;
165
166 let status = response.status.as_u16();
167 log::debug!("Response status: {status}");
168
169 if !response.status.is_success() {
170 let body = String::from_utf8_lossy(&response.body).to_string();
171 return if let Ok(error) = serde_json::from_str::<TardisErrorResponse>(&body) {
172 Err(Error::ApiError {
173 status,
174 code: error.code,
175 message: error.message,
176 })
177 } else {
178 Err(Error::ApiError {
179 status,
180 code: 0,
181 message: body,
182 })
183 };
184 }
185
186 let body = String::from_utf8_lossy(&response.body);
187 log::trace!("{body}");
188
189 if let Ok(instrument) = serde_json::from_str::<TardisInstrumentInfo>(&body) {
190 return Ok(vec![instrument]);
191 }
192
193 match serde_json::from_str(&body) {
194 Ok(parsed) => Ok(parsed),
195 Err(e) => {
196 log::error!("Failed to parse response: {e}");
197 log::debug!("Response body was: {body}");
198 Err(Error::ResponseParse(e.to_string()))
199 }
200 }
201 }
202
203 #[expect(clippy::too_many_arguments)]
211 pub async fn instruments(
212 &self,
213 exchange: TardisExchange,
214 symbol: Option<&str>,
215 filter: Option<&InstrumentFilter>,
216 start: Option<UnixNanos>,
217 end: Option<UnixNanos>,
218 available_offset: Option<UnixNanos>,
219 effective: Option<UnixNanos>,
220 ts_init: Option<UnixNanos>,
221 ) -> Result<Vec<InstrumentAny>> {
222 let response = self.instruments_info(exchange, symbol, filter).await?;
223
224 Ok(response
225 .into_iter()
226 .filter(|info| is_available(info, start, end, available_offset, effective))
227 .flat_map(|info| {
228 parse_instrument_any(&info, effective, ts_init, self.normalize_symbols)
229 })
230 .collect())
231 }
232
233 pub async fn bootstrap_instruments(
244 &self,
245 exchanges: &AHashSet<TardisExchange>,
246 ) -> Result<(
247 AHashMap<TardisInstrumentKey, Arc<TardisInstrumentMiniInfo>>,
248 Vec<InstrumentAny>,
249 )> {
250 let mut instrument_map: AHashMap<TardisInstrumentKey, Arc<TardisInstrumentMiniInfo>> =
251 AHashMap::new();
252 let mut nautilus_instruments: Vec<InstrumentAny> = Vec::new();
253
254 for exchange in exchanges {
255 log::debug!("Fetching instruments for {exchange}");
256
257 let instruments_info = match self.instruments_info(*exchange, None, None).await {
258 Ok(info) => info,
259 Err(e) => {
260 log::error!("Failed to fetch instruments for {exchange}: {e}");
261 continue;
262 }
263 };
264
265 log::debug!(
266 "Received {} instruments for {exchange}",
267 instruments_info.len()
268 );
269
270 for inst in &instruments_info {
271 let instrument_type = inst.instrument_type;
272 let price_precision = precision_from_str(&inst.price_increment.to_string());
273 let size_precision = precision_from_str(&inst.amount_increment.to_string());
274
275 let instrument_id = if self.normalize_symbols {
276 normalize_instrument_id(exchange, inst.id, &instrument_type, inst.inverse)
277 } else {
278 parse_instrument_id(exchange, inst.id)
279 };
280
281 let info = TardisInstrumentMiniInfo::new(
282 instrument_id,
283 Some(inst.id),
284 *exchange,
285 price_precision,
286 size_precision,
287 );
288 let key = info.as_tardis_instrument_key();
289 instrument_map.insert(key, Arc::new(info));
290 }
291
292 for inst in instruments_info {
293 nautilus_instruments.extend(parse_instrument_any(
294 &inst,
295 None,
296 None,
297 self.normalize_symbols,
298 ));
299 }
300 }
301
302 Ok((instrument_map, nautilus_instruments))
303 }
304}