1use std::{collections::HashMap, fmt::Debug};
19
20use async_trait::async_trait;
21use nautilus_common::providers::{InstrumentProvider, InstrumentStore};
22use nautilus_core::time::get_atomic_clock_realtime;
23use nautilus_model::{
24 identifiers::InstrumentId,
25 instruments::{Instrument, InstrumentAny},
26};
27
28use crate::{
29 common::{
30 consts::DERIVE_VENUE, enums::DeriveInstrumentType, parse::parse_derive_instrument_any,
31 },
32 http::{DeriveHttpClient, error::DeriveHttpError, models::DeriveInstrument},
33};
34
35const INSTRUMENT_NOT_FOUND_CODE: i64 = 12001;
36
37pub struct DeriveInstrumentProvider {
43 store: InstrumentStore,
44 http_client: DeriveHttpClient,
45 currencies: Vec<String>,
46 include_expired: bool,
47}
48
49impl Debug for DeriveInstrumentProvider {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct(stringify!(DeriveInstrumentProvider))
52 .field("store", &self.store)
53 .field("http_client", &self.http_client)
54 .field("currencies", &self.currencies)
55 .field("include_expired", &self.include_expired)
56 .finish()
57 }
58}
59
60impl DeriveInstrumentProvider {
61 #[must_use]
63 pub fn new(http_client: DeriveHttpClient, currencies: Vec<String>) -> Self {
64 Self {
65 store: InstrumentStore::new(),
66 http_client,
67 currencies,
68 include_expired: false,
69 }
70 }
71
72 #[must_use]
74 pub fn with_expired(
75 http_client: DeriveHttpClient,
76 currencies: Vec<String>,
77 include_expired: bool,
78 ) -> Self {
79 Self {
80 store: InstrumentStore::new(),
81 http_client,
82 currencies,
83 include_expired,
84 }
85 }
86
87 #[must_use]
89 pub fn currencies(&self) -> &[String] {
90 &self.currencies
91 }
92
93 #[must_use]
95 pub const fn include_expired(&self) -> bool {
96 self.include_expired
97 }
98
99 #[must_use]
101 pub const fn http_client(&self) -> &DeriveHttpClient {
102 &self.http_client
103 }
104
105 pub fn add_instruments(&mut self, instruments: Vec<InstrumentAny>) {
107 self.store.add_bulk(instruments);
108 }
109
110 async fn fetch_instruments(
111 &self,
112 currencies: &[String],
113 expired: bool,
114 ) -> anyhow::Result<Vec<InstrumentAny>> {
115 let mut instruments = Vec::new();
116
117 for currency in currencies {
118 let definitions =
119 fetch_instrument_definitions(&self.http_client, currency, expired).await?;
120 instruments.extend(parse_instrument_definitions(definitions));
121 }
122
123 Ok(instruments)
124 }
125}
126
127#[async_trait(?Send)]
128impl InstrumentProvider for DeriveInstrumentProvider {
129 fn store(&self) -> &InstrumentStore {
130 &self.store
131 }
132
133 fn store_mut(&mut self) -> &mut InstrumentStore {
134 &mut self.store
135 }
136
137 async fn load_all(&mut self, filters: Option<&HashMap<String, String>>) -> anyhow::Result<()> {
138 let (currencies, expired) =
139 resolve_load_filters(&self.currencies, self.include_expired, filters)?;
140 let instruments = self.fetch_instruments(¤cies, expired).await?;
141
142 self.store.clear();
143 self.add_instruments(instruments);
144 self.store.set_initialized();
145
146 Ok(())
147 }
148
149 async fn load_ids(
150 &mut self,
151 instrument_ids: &[InstrumentId],
152 filters: Option<&HashMap<String, String>>,
153 ) -> anyhow::Result<()> {
154 let missing: Vec<_> = instrument_ids
155 .iter()
156 .filter(|id| !self.store.contains(id))
157 .collect();
158
159 if missing.is_empty() {
160 return Ok(());
161 }
162
163 let expired = resolve_expired_filter(self.include_expired, filters)?;
164 let mut currencies: Vec<String> = missing
165 .iter()
166 .filter_map(|id| currency_from_instrument_id(id).map(ToOwned::to_owned))
167 .collect();
168 currencies.sort();
169 currencies.dedup();
170
171 if !currencies.is_empty() {
172 let instruments = self.fetch_instruments(¤cies, expired).await?;
173 self.add_instruments(instruments);
174 }
175
176 if missing.iter().all(|id| self.store.contains(id)) {
177 return Ok(());
178 }
179
180 if !self.store.is_initialized() {
181 let existing = self.store.get_all().values().cloned().collect::<Vec<_>>();
182 self.load_all(filters).await?;
183
184 for instrument in existing {
185 if !self.store.contains(&instrument.id()) {
186 self.store.add(instrument);
187 }
188 }
189 }
190
191 let missing_ids: Vec<_> = instrument_ids
192 .iter()
193 .filter(|id| !self.store.contains(id))
194 .collect();
195
196 if missing_ids.is_empty() {
197 Ok(())
198 } else {
199 anyhow::bail!("Derive instruments not found: {missing_ids:?}")
200 }
201 }
202
203 async fn load(
204 &mut self,
205 instrument_id: &InstrumentId,
206 filters: Option<&HashMap<String, String>>,
207 ) -> anyhow::Result<()> {
208 if self.store.contains(instrument_id) {
209 return Ok(());
210 }
211
212 self.load_ids(&[*instrument_id], filters).await
213 }
214}
215
216pub(crate) fn parse_instrument_definitions(
217 definitions: Vec<DeriveInstrument>,
218) -> Vec<InstrumentAny> {
219 let ts_init = get_atomic_clock_realtime().get_time_ns();
220 let mut instruments = Vec::with_capacity(definitions.len());
221
222 for definition in definitions {
223 match parse_derive_instrument_any(&definition, ts_init) {
224 Ok(Some(instrument)) => instruments.push(instrument),
225 Ok(None) => {}
226 Err(e) => log::warn!(
227 "Failed to parse Derive instrument {}: {e}",
228 definition.instrument_name,
229 ),
230 }
231 }
232
233 instruments
234}
235
236pub(crate) async fn fetch_instrument_definitions(
237 http_client: &DeriveHttpClient,
238 currency: &str,
239 expired: bool,
240) -> anyhow::Result<Vec<DeriveInstrument>> {
241 let (mut definitions, options, erc20s) = tokio::try_join!(
242 fetch_instruments_if_listed(http_client, currency, DeriveInstrumentType::Perp, expired),
243 fetch_instruments_if_listed(http_client, currency, DeriveInstrumentType::Option, expired,),
244 fetch_instruments_if_listed(http_client, currency, DeriveInstrumentType::Erc20, expired,),
245 )?;
246 definitions.extend(options);
247 definitions.extend(erc20s);
248
249 Ok(definitions)
250}
251
252async fn fetch_instruments_if_listed(
256 http_client: &DeriveHttpClient,
257 currency: &str,
258 instrument_type: DeriveInstrumentType,
259 expired: bool,
260) -> Result<Vec<DeriveInstrument>, DeriveHttpError> {
261 match http_client
262 .get_instruments(currency, instrument_type, expired)
263 .await
264 {
265 Ok(rows) => Ok(rows),
266 Err(DeriveHttpError::JsonRpc { code, .. }) if code == INSTRUMENT_NOT_FOUND_CODE => {
267 Ok(Vec::new())
268 }
269 Err(e) => Err(e),
270 }
271}
272
273fn resolve_load_filters(
274 default_currencies: &[String],
275 default_expired: bool,
276 filters: Option<&HashMap<String, String>>,
277) -> anyhow::Result<(Vec<String>, bool)> {
278 let currencies = filters
279 .and_then(|map| {
280 map.get("currency")
281 .map(|currency| vec![currency.trim().to_string()])
282 .or_else(|| map.get("currencies").map(|value| split_currencies(value)))
283 })
284 .unwrap_or_else(|| default_currencies.to_vec());
285
286 let currencies = normalize_currencies(currencies);
287
288 anyhow::ensure!(
289 !currencies.is_empty(),
290 "DeriveInstrumentProvider requires at least one currency",
291 );
292
293 let expired = resolve_expired_filter(default_expired, filters)?;
294
295 Ok((currencies, expired))
296}
297
298fn resolve_expired_filter(
299 default_expired: bool,
300 filters: Option<&HashMap<String, String>>,
301) -> anyhow::Result<bool> {
302 filters
303 .and_then(|map| map.get("expired"))
304 .map(|value| value.parse::<bool>())
305 .transpose()
306 .map_err(|e| anyhow::anyhow!("invalid Derive `expired` filter: {e}"))
307 .map(|value| value.unwrap_or(default_expired))
308}
309
310fn split_currencies(value: &str) -> Vec<String> {
311 normalize_currencies(value.split(',').map(ToOwned::to_owned).collect())
312}
313
314fn normalize_currencies(currencies: Vec<String>) -> Vec<String> {
315 let mut currencies: Vec<_> = currencies
316 .into_iter()
317 .map(|currency| currency.trim().to_string())
318 .filter(|currency| !currency.is_empty())
319 .collect();
320 currencies.sort();
321 currencies.dedup();
322 currencies
323}
324
325fn currency_from_instrument_id(instrument_id: &InstrumentId) -> Option<&str> {
326 if instrument_id.venue != *DERIVE_VENUE {
327 return None;
328 }
329
330 instrument_id
331 .symbol
332 .as_str()
333 .split_once('-')
334 .and_then(|(currency, _)| (!currency.is_empty()).then_some(currency))
335}