1use databento::dbn;
17use nautilus_core::UnixNanos;
18use nautilus_model::{
19 enums::AssetClass,
20 identifiers::{InstrumentId, Symbol},
21 instruments::{
22 CurrencyPair, Equity, FuturesContract, FuturesSpread, InstrumentAny, OptionContract,
23 OptionSpread,
24 },
25 types::Currency,
26};
27use ustr::Ustr;
28
29use super::{
30 expiration::{DatabentoDecodeConfig, corrected_option_expiration},
31 primitives::{
32 decode_lot_size, decode_multiplier, decode_optional_timestamp, decode_price,
33 decode_price_increment, decode_timestamp, decode_underlying, parse_cfi_iso10926,
34 parse_currency_or_usd_default, parse_option_kind,
35 },
36};
37
38pub fn decode_instrument_def_msg(
45 msg: &dbn::InstrumentDefMsg,
46 instrument_id: InstrumentId,
47 ts_init: Option<UnixNanos>,
48 decode_config: Option<&DatabentoDecodeConfig>,
49) -> anyhow::Result<Option<InstrumentAny>> {
50 match msg.instrument_class as u8 as char {
51 'K' => Ok(Some(InstrumentAny::Equity(decode_equity(
52 msg,
53 instrument_id,
54 ts_init,
55 )?))),
56 'X' => {
57 Ok(decode_currency_pair(msg, instrument_id, ts_init)?.map(InstrumentAny::CurrencyPair))
58 }
59 'F' => Ok(Some(InstrumentAny::FuturesContract(
60 decode_futures_contract(msg, instrument_id, ts_init)?,
61 ))),
62 'S' => Ok(Some(InstrumentAny::FuturesSpread(decode_futures_spread(
63 msg,
64 instrument_id,
65 ts_init,
66 )?))),
67 'C' | 'P' => Ok(Some(InstrumentAny::OptionContract(decode_option_contract(
68 msg,
69 instrument_id,
70 ts_init,
71 decode_config,
72 )?))),
73 'T' | 'M' => Ok(Some(InstrumentAny::OptionSpread(decode_option_spread(
74 msg,
75 instrument_id,
76 ts_init,
77 decode_config,
78 )?))),
79 other => {
80 let label = match other {
81 'I' => "'I' (Index)".to_string(),
82 'B' => "'B' (Bond)".to_string(),
83 _ => format!("'{other}'"),
84 };
85 log::warn!("Skipping unsupported `instrument_class` {label} for {instrument_id}",);
86 Ok(None)
87 }
88 }
89}
90
91fn decode_currency_pair(
92 msg: &dbn::InstrumentDefMsg,
93 instrument_id: InstrumentId,
94 ts_init: Option<UnixNanos>,
95) -> anyhow::Result<Option<CurrencyPair>> {
96 let raw_symbol_str = msg.raw_symbol()?;
97 let raw_symbol = Symbol::from(raw_symbol_str);
98 let Some((base_currency, quote_currency)) = parse_fx_pair(
99 raw_symbol_str,
100 msg.asset().unwrap_or_default(),
101 msg.currency().unwrap_or_default(),
102 ) else {
103 log::warn!(
104 "Skipping FX spot {instrument_id}: could not parse currencies from raw_symbol='{raw_symbol_str}'"
105 );
106 return Ok(None);
107 };
108 let price_increment = decode_price_increment(msg.min_price_increment, quote_currency.precision);
109 let size_increment = decode_lot_size(msg.min_lot_size_round_lot);
110 let multiplier = decode_multiplier(msg.unit_of_measure_qty)?;
111 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
112 let ts_event = UnixNanos::from(msg.ts_recv);
113 let ts_init = ts_init.unwrap_or(ts_event);
114
115 Ok(Some(CurrencyPair::new_checked(
116 instrument_id,
117 raw_symbol,
118 base_currency,
119 quote_currency,
120 price_increment.precision,
121 size_increment.precision,
122 price_increment,
123 size_increment,
124 Some(multiplier),
125 Some(lot_size),
126 None,
127 None,
128 None,
129 None,
130 None,
131 None,
132 None,
133 None,
134 None,
135 None,
136 None,
137 None,
138 ts_event,
139 ts_init,
140 )?))
141}
142
143fn parse_fx_pair(raw_symbol: &str, asset: &str, currency: &str) -> Option<(Currency, Currency)> {
144 parse_fx_pair_from_symbol(raw_symbol)
145 .or_else(|| parse_fx_pair_from_symbol(asset))
146 .or_else(|| parse_fx_pair_from_asset_currency(asset, currency))
147}
148
149fn parse_fx_pair_from_symbol(value: &str) -> Option<(Currency, Currency)> {
150 let normalized = value
151 .chars()
152 .filter(|ch| ch.is_ascii_alphabetic())
153 .collect::<String>()
154 .to_ascii_uppercase();
155
156 if normalized.len() != 6 {
157 return None;
158 }
159
160 let base = Currency::try_from_str(&normalized[..3])?;
161 let quote = Currency::try_from_str(&normalized[3..])?;
162 Some((base, quote))
163}
164
165fn parse_fx_pair_from_asset_currency(asset: &str, currency: &str) -> Option<(Currency, Currency)> {
166 let base = Currency::try_from_str(asset.trim().to_ascii_uppercase().as_str())?;
167 let quote = Currency::try_from_str(currency.trim().to_ascii_uppercase().as_str())?;
168 Some((base, quote))
169}
170
171pub fn decode_equity(
177 msg: &dbn::InstrumentDefMsg,
178 instrument_id: InstrumentId,
179 ts_init: Option<UnixNanos>,
180) -> anyhow::Result<Equity> {
181 let currency = parse_currency_or_usd_default(msg.currency());
182 let price_increment = decode_price_increment(msg.min_price_increment, currency.precision);
183 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
184 let ts_event = UnixNanos::from(msg.ts_recv); let ts_init = ts_init.unwrap_or(ts_event);
186
187 Ok(Equity::new(
188 instrument_id,
189 instrument_id.symbol,
190 None, currency,
192 price_increment.precision,
193 price_increment,
194 Some(lot_size),
195 None, None, None, None, None, None, None, None, None, None, ts_event,
206 ts_init,
207 ))
208}
209
210pub fn decode_futures_contract(
216 msg: &dbn::InstrumentDefMsg,
217 instrument_id: InstrumentId,
218 ts_init: Option<UnixNanos>,
219) -> anyhow::Result<FuturesContract> {
220 let currency = parse_currency_or_usd_default(msg.currency());
221 let exchange = Ustr::from(msg.exchange()?);
222 let underlying = decode_underlying(msg.asset()?, &instrument_id.symbol);
223 let (asset_class, _) = parse_cfi_iso10926(msg.cfi()?);
224 let price_increment = decode_price_increment(msg.min_price_increment, currency.precision);
225 let multiplier = decode_multiplier(msg.unit_of_measure_qty)?;
226 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
227 let ts_event = UnixNanos::from(msg.ts_recv); let ts_init = ts_init.unwrap_or(ts_event);
229
230 Ok(FuturesContract::new_checked(
231 instrument_id,
232 instrument_id.symbol,
233 asset_class.unwrap_or(AssetClass::Commodity),
234 Some(exchange),
235 underlying,
236 decode_optional_timestamp(msg.activation).unwrap_or_default(),
237 decode_timestamp(msg.expiration, "expiration")?,
238 currency,
239 price_increment.precision,
240 price_increment,
241 multiplier,
242 lot_size,
243 None, None, None, None, None, None, None, None, None, None, ts_event,
254 ts_init,
255 )?)
256}
257
258pub fn decode_futures_spread(
264 msg: &dbn::InstrumentDefMsg,
265 instrument_id: InstrumentId,
266 ts_init: Option<UnixNanos>,
267) -> anyhow::Result<FuturesSpread> {
268 let exchange = Ustr::from(msg.exchange()?);
269 let underlying = decode_underlying(msg.asset()?, &instrument_id.symbol);
270 let (asset_class, _) = parse_cfi_iso10926(msg.cfi()?);
271 let strategy_type = Ustr::from(msg.secsubtype()?);
272 let currency = parse_currency_or_usd_default(msg.currency());
273 let price_increment = decode_price_increment(msg.min_price_increment, currency.precision);
274 let multiplier = decode_multiplier(msg.unit_of_measure_qty)?;
275 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
276 let ts_event = UnixNanos::from(msg.ts_recv); let ts_init = ts_init.unwrap_or(ts_event);
278
279 Ok(FuturesSpread::new_checked(
280 instrument_id,
281 instrument_id.symbol,
282 asset_class.unwrap_or(AssetClass::Commodity),
283 Some(exchange),
284 underlying,
285 strategy_type,
286 decode_optional_timestamp(msg.activation).unwrap_or_default(),
287 decode_timestamp(msg.expiration, "expiration")?,
288 currency,
289 price_increment.precision,
290 price_increment,
291 multiplier,
292 lot_size,
293 None, None, None, None, None, None, None, None, None, None, ts_event,
304 ts_init,
305 )?)
306}
307
308pub fn decode_option_contract(
314 msg: &dbn::InstrumentDefMsg,
315 instrument_id: InstrumentId,
316 ts_init: Option<UnixNanos>,
317 decode_config: Option<&DatabentoDecodeConfig>,
318) -> anyhow::Result<OptionContract> {
319 let currency = parse_currency_or_usd_default(msg.currency());
320 let strike_price_currency = parse_currency_or_usd_default(msg.strike_price_currency());
321 let exchange = Ustr::from(msg.exchange()?);
322 let underlying = decode_underlying(msg.underlying()?, &instrument_id.symbol);
323 let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" {
324 Some(AssetClass::Equity)
325 } else {
326 let (asset_class, _) = parse_cfi_iso10926(msg.cfi()?);
327 asset_class
328 };
329 let option_kind = parse_option_kind(msg.instrument_class)?;
330 let strike_price = decode_price(
331 msg.strike_price,
332 strike_price_currency.precision,
333 "strike_price",
334 )?;
335 let price_increment = decode_price_increment(msg.min_price_increment, currency.precision);
336 let multiplier = decode_multiplier(msg.unit_of_measure_qty)?;
337 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
338 let expiration = corrected_option_expiration(
339 decode_timestamp(msg.expiration, "expiration")?,
340 underlying,
341 msg.hd.publisher().ok().map(|p| p.dataset()),
342 decode_config,
343 );
344 let ts_event = UnixNanos::from(msg.ts_recv); let ts_init = ts_init.unwrap_or(ts_event);
346
347 Ok(OptionContract::new_checked(
348 instrument_id,
349 instrument_id.symbol,
350 asset_class_opt.unwrap_or(AssetClass::Commodity),
351 Some(exchange),
352 underlying,
353 option_kind,
354 strike_price,
355 currency,
356 decode_optional_timestamp(msg.activation).unwrap_or_default(),
357 expiration,
358 price_increment.precision,
359 price_increment,
360 multiplier,
361 lot_size,
362 None, None, None, None, None, None, None, None, None, None, ts_event,
373 ts_init,
374 )?)
375}
376
377pub fn decode_option_spread(
383 msg: &dbn::InstrumentDefMsg,
384 instrument_id: InstrumentId,
385 ts_init: Option<UnixNanos>,
386 decode_config: Option<&DatabentoDecodeConfig>,
387) -> anyhow::Result<OptionSpread> {
388 let exchange = Ustr::from(msg.exchange()?);
389 let underlying = decode_underlying(msg.underlying()?, &instrument_id.symbol);
390 let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" {
391 Some(AssetClass::Equity)
392 } else {
393 let (asset_class, _) = parse_cfi_iso10926(msg.cfi()?);
394 asset_class
395 };
396 let strategy_type = Ustr::from(msg.secsubtype()?);
397 let currency = parse_currency_or_usd_default(msg.currency());
398 let price_increment = decode_price_increment(msg.min_price_increment, currency.precision);
399 let multiplier = decode_multiplier(msg.unit_of_measure_qty)?;
400 let lot_size = decode_lot_size(msg.min_lot_size_round_lot);
401 let expiration = corrected_option_expiration(
402 decode_timestamp(msg.expiration, "expiration")?,
403 underlying,
404 msg.hd.publisher().ok().map(|p| p.dataset()),
405 decode_config,
406 );
407 let ts_event = msg.ts_recv.into(); let ts_init = ts_init.unwrap_or(ts_event);
409
410 Ok(OptionSpread::new_checked(
411 instrument_id,
412 instrument_id.symbol,
413 asset_class_opt.unwrap_or(AssetClass::Commodity),
414 Some(exchange),
415 underlying,
416 strategy_type,
417 decode_optional_timestamp(msg.activation).unwrap_or_default(),
418 expiration,
419 currency,
420 price_increment.precision,
421 price_increment,
422 multiplier,
423 lot_size,
424 None, None, None, None, None, None, None, None, None, None, ts_event,
435 ts_init,
436 )?)
437}