nautilus_tardis/http/
client.rs1use std::{env, time::Duration};
17
18use nautilus_core::{UnixNanos, consts::NAUTILUS_USER_AGENT};
19use nautilus_model::instruments::InstrumentAny;
20use reqwest::Response;
21
22use super::{
23 TARDIS_BASE_URL,
24 error::{Error, TardisErrorResponse},
25 instruments::is_available,
26 models::TardisInstrumentInfo,
27 parse::parse_instrument_any,
28 query::InstrumentFilter,
29};
30use crate::enums::TardisExchange;
31
32pub type Result<T> = std::result::Result<T, Error>;
33
34#[cfg_attr(
37 feature = "python",
38 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
39)]
40#[derive(Debug, Clone)]
41pub struct TardisHttpClient {
42 base_url: String,
43 api_key: String,
44 client: reqwest::Client,
45 normalize_symbols: bool,
46}
47
48impl TardisHttpClient {
49 pub fn new(
56 api_key: Option<&str>,
57 base_url: Option<&str>,
58 timeout_secs: Option<u64>,
59 normalize_symbols: bool,
60 ) -> anyhow::Result<Self> {
61 let api_key = match api_key {
62 Some(key) => key.to_string(),
63 None => env::var("TARDIS_API_KEY").map_err(|_| {
64 anyhow::anyhow!(
65 "API key must be provided or set in the 'TARDIS_API_KEY' environment variable"
66 )
67 })?,
68 };
69
70 let base_url = base_url.map_or_else(|| TARDIS_BASE_URL.to_string(), ToString::to_string);
71 let timeout = timeout_secs.map_or_else(|| Duration::from_secs(60), Duration::from_secs);
72
73 let client = reqwest::Client::builder()
74 .user_agent(NAUTILUS_USER_AGENT)
75 .timeout(timeout)
76 .build()?;
77
78 Ok(Self {
79 base_url,
80 api_key,
81 client,
82 normalize_symbols,
83 })
84 }
85
86 async fn handle_error_response<T>(resp: Response) -> Result<T> {
87 let status = resp.status().as_u16();
88 let error_text = match resp.text().await {
89 Ok(text) => text,
90 Err(e) => {
91 tracing::warn!("Failed to extract error response body: {e}");
92 String::from("Failed to extract error response")
93 }
94 };
95
96 if let Ok(error) = serde_json::from_str::<TardisErrorResponse>(&error_text) {
97 Err(Error::ApiError {
98 status,
99 code: error.code,
100 message: error.message,
101 })
102 } else {
103 Err(Error::ApiError {
104 status,
105 code: 0,
106 message: error_text,
107 })
108 }
109 }
110
111 pub async fn instruments_info(
119 &self,
120 exchange: TardisExchange,
121 symbol: Option<&str>,
122 filter: Option<&InstrumentFilter>,
123 ) -> Result<Vec<TardisInstrumentInfo>> {
124 let mut url = format!("{}/instruments/{exchange}", &self.base_url);
125 if let Some(symbol) = symbol {
126 url.push_str(&format!("/{symbol}"));
127 }
128 if let Some(filter) = filter
129 && let Ok(filter_json) = serde_json::to_string(filter)
130 {
131 url.push_str(&format!("?filter={}", urlencoding::encode(&filter_json)));
132 }
133 tracing::debug!("Requesting: {url}");
134
135 let resp = self
136 .client
137 .get(url)
138 .bearer_auth(&self.api_key)
139 .send()
140 .await?;
141 tracing::debug!("Response status: {}", resp.status());
142
143 if !resp.status().is_success() {
144 return Self::handle_error_response(resp).await;
145 }
146
147 let body = resp.text().await?;
148 tracing::trace!("{body}");
149
150 if let Ok(instrument) = serde_json::from_str::<TardisInstrumentInfo>(&body) {
151 return Ok(vec![instrument]);
152 }
153
154 match serde_json::from_str(&body) {
155 Ok(parsed) => Ok(parsed),
156 Err(e) => {
157 tracing::error!("Failed to parse response: {e}");
158 tracing::debug!("Response body was: {body}");
159 Err(Error::ResponseParse(e.to_string()))
160 }
161 }
162 }
163
164 #[allow(clippy::too_many_arguments)]
172 pub async fn instruments(
173 &self,
174 exchange: TardisExchange,
175 symbol: Option<&str>,
176 filter: Option<&InstrumentFilter>,
177 start: Option<UnixNanos>,
178 end: Option<UnixNanos>,
179 available_offset: Option<UnixNanos>,
180 effective: Option<UnixNanos>,
181 ts_init: Option<UnixNanos>,
182 ) -> Result<Vec<InstrumentAny>> {
183 let response = self.instruments_info(exchange, symbol, filter).await?;
184
185 Ok(response
186 .into_iter()
187 .filter(|info| is_available(info, start, end, available_offset, effective))
188 .flat_map(|info| parse_instrument_any(info, effective, ts_init, self.normalize_symbols))
189 .collect())
190 }
191}