Skip to main content

nautilus_tardis/http/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use jiff::Timestamp;
17use nautilus_core::UnixNanos;
18use nautilus_model::{
19    identifiers::Symbol,
20    instruments::InstrumentAny,
21    types::{Currency, Price, Quantity},
22};
23use rust_decimal::Decimal;
24use rust_decimal_macros::dec;
25
26use super::{
27    instruments::{
28        create_crypto_future, create_crypto_option, create_crypto_perpetual, create_currency_pair,
29        get_currency,
30    },
31    models::TardisInstrumentInfo,
32};
33use crate::common::{
34    enums::TardisInstrumentType,
35    parse::{normalize_instrument_id, parse_instrument_id},
36};
37
38#[must_use]
39pub fn parse_instrument_any(
40    info: &TardisInstrumentInfo,
41    effective: Option<UnixNanos>,
42    ts_init: Option<UnixNanos>,
43    normalize_symbols: bool,
44) -> Vec<InstrumentAny> {
45    match info.instrument_type {
46        TardisInstrumentType::Spot => {
47            parse_spot_instrument(info, effective, ts_init, normalize_symbols)
48        }
49        TardisInstrumentType::Perpetual => {
50            parse_perp_instrument(info, effective, ts_init, normalize_symbols)
51        }
52        TardisInstrumentType::Future | TardisInstrumentType::Combo => {
53            parse_future_instrument(info, effective, ts_init, normalize_symbols)
54        }
55        TardisInstrumentType::Option => {
56            match parse_option_instrument(info, effective, ts_init, normalize_symbols) {
57                Ok(instruments) => instruments,
58                Err(e) => {
59                    log::error!("Failed to parse option instrument: {e}");
60                    vec![]
61                }
62            }
63        }
64    }
65}
66
67fn parse_spot_instrument(
68    info: &TardisInstrumentInfo,
69    effective: Option<UnixNanos>,
70    ts_init: Option<UnixNanos>,
71    normalize_symbols: bool,
72) -> Vec<InstrumentAny> {
73    let instrument_id = if normalize_symbols {
74        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
75    } else {
76        parse_instrument_id(&info.exchange, info.id)
77    };
78    let raw_symbol = Symbol::new(info.id);
79    let margin_init = dec!(0); // TBD
80    let margin_maint = dec!(0); // TBD
81
82    let mut price_increment = parse_price_increment(info.price_increment);
83    let base_currency = get_currency(info.base_currency.to_uppercase().as_str());
84    let mut size_increment = parse_spot_size_increment(info.amount_increment, base_currency);
85    let mut multiplier = parse_multiplier(info.contract_multiplier);
86    let mut maker_fee = parse_fee_rate(info.maker_fee);
87    let mut taker_fee = parse_fee_rate(info.taker_fee);
88    let mut ts_event = info
89        .changes
90        .as_ref()
91        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
92        .unwrap_or_else(|| UnixNanos::from(info.available_since));
93
94    // Current instrument definition
95    let mut instruments = vec![create_currency_pair(
96        info,
97        instrument_id,
98        raw_symbol,
99        price_increment,
100        size_increment,
101        multiplier,
102        margin_init,
103        margin_maint,
104        maker_fee,
105        taker_fee,
106        ts_event,
107        ts_init.unwrap_or(ts_event),
108    )];
109
110    if let Some(changes) = &info.changes {
111        // Sort changes newest to oldest
112        let mut sorted_changes = changes.clone();
113        sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
114
115        if let Some(effective_time) = effective {
116            // Apply changes where change.until >= effective_time
117            for (i, change) in sorted_changes.iter().enumerate() {
118                if change.price_increment.is_none()
119                    && change.amount_increment.is_none()
120                    && change.contract_multiplier.is_none()
121                {
122                    continue; // No changes to apply (already pushed current definition)
123                }
124
125                ts_event = UnixNanos::from(change.until);
126
127                if ts_event < effective_time {
128                    break; // Early exit since changes are sorted newest to oldest
129                } else if i == sorted_changes.len() - 1 {
130                    ts_event = UnixNanos::from(info.available_since);
131                }
132
133                price_increment = change
134                    .price_increment
135                    .map_or(price_increment, parse_price_increment);
136                size_increment = change.amount_increment.map_or(size_increment, |value| {
137                    parse_spot_size_increment(value, base_currency)
138                });
139                multiplier = match change.contract_multiplier {
140                    Some(value) => Some(Quantity::from(value.to_string())),
141                    None => multiplier,
142                };
143                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
144                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
145            }
146
147            // Replace with single instrument reflecting effective state
148            instruments = vec![create_currency_pair(
149                info,
150                instrument_id,
151                raw_symbol,
152                price_increment,
153                size_increment,
154                multiplier,
155                margin_init,
156                margin_maint,
157                maker_fee,
158                taker_fee,
159                ts_event,
160                ts_init.unwrap_or(ts_event),
161            )];
162        } else {
163            // Historical sequence with all states
164            for (i, change) in sorted_changes.iter().enumerate() {
165                if change.price_increment.is_none()
166                    && change.amount_increment.is_none()
167                    && change.contract_multiplier.is_none()
168                {
169                    continue; // No changes to apply (already pushed current definition)
170                }
171
172                price_increment = change
173                    .price_increment
174                    .map_or(price_increment, parse_price_increment);
175                size_increment = change.amount_increment.map_or(size_increment, |value| {
176                    parse_spot_size_increment(value, base_currency)
177                });
178                multiplier = match change.contract_multiplier {
179                    Some(value) => Some(Quantity::from(value.to_string())),
180                    None => multiplier,
181                };
182                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
183                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
184
185                // Get the timestamp for when the change occurred
186                ts_event = if i == sorted_changes.len() - 1 {
187                    UnixNanos::from(info.available_since)
188                } else {
189                    UnixNanos::from(change.until)
190                };
191
192                instruments.push(create_currency_pair(
193                    info,
194                    instrument_id,
195                    raw_symbol,
196                    price_increment,
197                    size_increment,
198                    multiplier,
199                    margin_init,
200                    margin_maint,
201                    maker_fee,
202                    taker_fee,
203                    ts_event,
204                    ts_init.unwrap_or(ts_event),
205                ));
206            }
207
208            // Sort in ascending (chronological) order
209            instruments.reverse();
210        }
211    }
212
213    instruments
214}
215
216fn parse_perp_instrument(
217    info: &TardisInstrumentInfo,
218    effective: Option<UnixNanos>,
219    ts_init: Option<UnixNanos>,
220    normalize_symbols: bool,
221) -> Vec<InstrumentAny> {
222    let instrument_id = if normalize_symbols {
223        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
224    } else {
225        parse_instrument_id(&info.exchange, info.id)
226    };
227    let raw_symbol = Symbol::new(info.id);
228    let margin_init = dec!(0); // TBD
229    let margin_maint = dec!(0); // TBD
230
231    let mut price_increment = parse_price_increment(info.price_increment);
232    let mut size_increment = parse_size_increment(info.amount_increment);
233    let mut multiplier = parse_multiplier(info.contract_multiplier);
234    let mut maker_fee = parse_fee_rate(info.maker_fee);
235    let mut taker_fee = parse_fee_rate(info.taker_fee);
236    let mut ts_event = info
237        .changes
238        .as_ref()
239        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
240        .unwrap_or_else(|| UnixNanos::from(info.available_since));
241
242    // Current instrument definition
243    let mut instruments = vec![create_crypto_perpetual(
244        info,
245        instrument_id,
246        raw_symbol,
247        price_increment,
248        size_increment,
249        multiplier,
250        margin_init,
251        margin_maint,
252        maker_fee,
253        taker_fee,
254        ts_event,
255        ts_init.unwrap_or(ts_event),
256    )];
257
258    if let Some(changes) = &info.changes {
259        // Sort changes newest to oldest
260        let mut sorted_changes = changes.clone();
261        sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
262
263        if let Some(effective_time) = effective {
264            // Apply changes where change.until >= effective_time
265            for (i, change) in sorted_changes.iter().enumerate() {
266                if change.price_increment.is_none()
267                    && change.amount_increment.is_none()
268                    && change.contract_multiplier.is_none()
269                {
270                    continue; // No changes to apply (already pushed current definition)
271                }
272
273                ts_event = UnixNanos::from(change.until);
274
275                if ts_event < effective_time {
276                    break; // Early exit since changes are sorted newest to oldest
277                } else if i == sorted_changes.len() - 1 {
278                    ts_event = UnixNanos::from(info.available_since);
279                }
280
281                price_increment = change
282                    .price_increment
283                    .map_or(price_increment, parse_price_increment);
284                size_increment = change
285                    .amount_increment
286                    .map_or(size_increment, parse_size_increment);
287                multiplier = match change.contract_multiplier {
288                    Some(value) => Some(Quantity::from(value.to_string())),
289                    None => multiplier,
290                };
291                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
292                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
293            }
294
295            // Replace with single instrument reflecting effective state
296            instruments = vec![create_crypto_perpetual(
297                info,
298                instrument_id,
299                raw_symbol,
300                price_increment,
301                size_increment,
302                multiplier,
303                margin_init,
304                margin_maint,
305                maker_fee,
306                taker_fee,
307                ts_event,
308                ts_init.unwrap_or(ts_event),
309            )];
310        } else {
311            // Historical view with all states
312            for (i, change) in sorted_changes.iter().enumerate() {
313                if change.price_increment.is_none()
314                    && change.amount_increment.is_none()
315                    && change.contract_multiplier.is_none()
316                {
317                    continue; // No changes to apply (already pushed current definition)
318                }
319
320                price_increment = change
321                    .price_increment
322                    .map_or(price_increment, parse_price_increment);
323                size_increment = change
324                    .amount_increment
325                    .map_or(size_increment, parse_size_increment);
326                multiplier = match change.contract_multiplier {
327                    Some(value) => Some(Quantity::from(value.to_string())),
328                    None => multiplier,
329                };
330                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
331                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
332
333                // Get the timestamp for when the change occurred
334                ts_event = if i == sorted_changes.len() - 1 {
335                    UnixNanos::from(info.available_since)
336                } else {
337                    UnixNanos::from(change.until)
338                };
339
340                instruments.push(create_crypto_perpetual(
341                    info,
342                    instrument_id,
343                    raw_symbol,
344                    price_increment,
345                    size_increment,
346                    multiplier,
347                    margin_init,
348                    margin_maint,
349                    maker_fee,
350                    taker_fee,
351                    ts_event,
352                    ts_init.unwrap_or(ts_event),
353                ));
354            }
355
356            // Sort in ascending (chronological) order
357            instruments.reverse();
358        }
359    }
360
361    instruments
362}
363
364fn parse_future_instrument(
365    info: &TardisInstrumentInfo,
366    effective: Option<UnixNanos>,
367    ts_init: Option<UnixNanos>,
368    normalize_symbols: bool,
369) -> Vec<InstrumentAny> {
370    let instrument_id = if normalize_symbols {
371        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
372    } else {
373        parse_instrument_id(&info.exchange, info.id)
374    };
375    let raw_symbol = Symbol::new(info.id);
376    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
377    let expiration = parse_datetime_to_unix_nanos(info.expiry);
378    let margin_init = dec!(0); // TBD
379    let margin_maint = dec!(0); // TBD
380
381    let mut price_increment = parse_price_increment(info.price_increment);
382    let mut size_increment = parse_size_increment(info.amount_increment);
383    let mut multiplier = parse_multiplier(info.contract_multiplier);
384    let mut maker_fee = parse_fee_rate(info.maker_fee);
385    let mut taker_fee = parse_fee_rate(info.taker_fee);
386    let mut ts_event = info
387        .changes
388        .as_ref()
389        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
390        .unwrap_or_else(|| UnixNanos::from(info.available_since));
391
392    // Current instrument definition
393    let mut instruments = vec![create_crypto_future(
394        info,
395        instrument_id,
396        raw_symbol,
397        activation,
398        expiration,
399        price_increment,
400        size_increment,
401        multiplier,
402        margin_init,
403        margin_maint,
404        maker_fee,
405        taker_fee,
406        ts_event,
407        ts_init.unwrap_or(ts_event),
408    )];
409
410    if let Some(changes) = &info.changes {
411        // Sort changes newest to oldest
412        let mut sorted_changes = changes.clone();
413        sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
414
415        if let Some(effective_time) = effective {
416            // Apply changes where change.until >= effective_time
417            for (i, change) in sorted_changes.iter().enumerate() {
418                if change.price_increment.is_none()
419                    && change.amount_increment.is_none()
420                    && change.contract_multiplier.is_none()
421                {
422                    continue; // No changes to apply (already pushed current definition)
423                }
424
425                ts_event = UnixNanos::from(change.until);
426
427                if ts_event < effective_time {
428                    break; // Early exit since changes are sorted newest to oldest
429                } else if i == sorted_changes.len() - 1 {
430                    ts_event = UnixNanos::from(info.available_since);
431                }
432
433                price_increment = change
434                    .price_increment
435                    .map_or(price_increment, parse_price_increment);
436                size_increment = change
437                    .amount_increment
438                    .map_or(size_increment, parse_size_increment);
439                multiplier = match change.contract_multiplier {
440                    Some(value) => Some(Quantity::from(value.to_string())),
441                    None => multiplier,
442                };
443                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
444                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
445            }
446
447            // Replace with single instrument reflecting effective state
448            instruments = vec![create_crypto_future(
449                info,
450                instrument_id,
451                raw_symbol,
452                activation,
453                expiration,
454                price_increment,
455                size_increment,
456                multiplier,
457                margin_init,
458                margin_maint,
459                maker_fee,
460                taker_fee,
461                ts_event,
462                ts_init.unwrap_or(ts_event),
463            )];
464        } else {
465            // Historical view with all states
466            for (i, change) in sorted_changes.iter().enumerate() {
467                if change.price_increment.is_none()
468                    && change.amount_increment.is_none()
469                    && change.contract_multiplier.is_none()
470                {
471                    continue; // No changes to apply (already pushed current definition)
472                }
473
474                price_increment = change
475                    .price_increment
476                    .map_or(price_increment, parse_price_increment);
477                size_increment = change
478                    .amount_increment
479                    .map_or(size_increment, parse_size_increment);
480                multiplier = match change.contract_multiplier {
481                    Some(value) => Some(Quantity::from(value.to_string())),
482                    None => multiplier,
483                };
484                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
485                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
486
487                // Get the timestamp for when the change occurred
488                ts_event = if i == sorted_changes.len() - 1 {
489                    UnixNanos::from(info.available_since)
490                } else {
491                    UnixNanos::from(change.until)
492                };
493
494                instruments.push(create_crypto_future(
495                    info,
496                    instrument_id,
497                    raw_symbol,
498                    activation,
499                    expiration,
500                    price_increment,
501                    size_increment,
502                    multiplier,
503                    margin_init,
504                    margin_maint,
505                    maker_fee,
506                    taker_fee,
507                    ts_event,
508                    ts_init.unwrap_or(ts_event),
509                ));
510            }
511
512            // Sort in ascending (chronological) order
513            instruments.reverse();
514        }
515    }
516
517    instruments
518}
519
520fn parse_option_instrument(
521    info: &TardisInstrumentInfo,
522    effective: Option<UnixNanos>,
523    ts_init: Option<UnixNanos>,
524    normalize_symbols: bool,
525) -> anyhow::Result<Vec<InstrumentAny>> {
526    let instrument_id = if normalize_symbols {
527        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
528    } else {
529        parse_instrument_id(&info.exchange, info.id)
530    };
531    let raw_symbol = Symbol::new(info.id);
532    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
533    let expiration = parse_datetime_to_unix_nanos(info.expiry);
534    let margin_init = dec!(0); // TBD
535    let margin_maint = dec!(0); // TBD
536
537    let mut price_increment = parse_price_increment(info.price_increment);
538    let mut size_increment = parse_size_increment(info.amount_increment);
539    let mut multiplier = parse_multiplier(info.contract_multiplier);
540    let mut maker_fee = parse_fee_rate(info.maker_fee);
541    let mut taker_fee = parse_fee_rate(info.taker_fee);
542    let mut ts_event = info
543        .changes
544        .as_ref()
545        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
546        .unwrap_or_else(|| UnixNanos::from(info.available_since));
547
548    // Current instrument definition
549    let mut instruments = vec![create_crypto_option(
550        info,
551        instrument_id,
552        raw_symbol,
553        activation,
554        expiration,
555        price_increment,
556        size_increment,
557        multiplier,
558        margin_init,
559        margin_maint,
560        maker_fee,
561        taker_fee,
562        ts_event,
563        ts_init.unwrap_or(ts_event),
564    )?];
565
566    if let Some(changes) = &info.changes {
567        // Sort changes newest to oldest
568        let mut sorted_changes = changes.clone();
569        sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
570
571        if let Some(effective_time) = effective {
572            // Apply changes where change.until >= effective_time
573            for (i, change) in sorted_changes.iter().enumerate() {
574                if change.price_increment.is_none()
575                    && change.amount_increment.is_none()
576                    && change.contract_multiplier.is_none()
577                {
578                    continue; // No changes to apply (already pushed current definition)
579                }
580
581                ts_event = UnixNanos::from(change.until);
582
583                if ts_event < effective_time {
584                    break; // Early exit since changes are sorted newest to oldest
585                } else if i == sorted_changes.len() - 1 {
586                    ts_event = UnixNanos::from(info.available_since);
587                }
588
589                price_increment = change
590                    .price_increment
591                    .map_or(price_increment, parse_price_increment);
592                size_increment = change
593                    .amount_increment
594                    .map_or(size_increment, parse_size_increment);
595                multiplier = match change.contract_multiplier {
596                    Some(value) => Some(Quantity::from(value.to_string())),
597                    None => multiplier,
598                };
599                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
600                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
601            }
602
603            // Replace with single instrument reflecting effective state
604            instruments = vec![create_crypto_option(
605                info,
606                instrument_id,
607                raw_symbol,
608                activation,
609                expiration,
610                price_increment,
611                size_increment,
612                multiplier,
613                margin_init,
614                margin_maint,
615                maker_fee,
616                taker_fee,
617                ts_event,
618                ts_init.unwrap_or(ts_event),
619            )?];
620        } else {
621            // Historical view with all states
622            for (i, change) in sorted_changes.iter().enumerate() {
623                if change.price_increment.is_none()
624                    && change.amount_increment.is_none()
625                    && change.contract_multiplier.is_none()
626                {
627                    continue; // No changes to apply (already pushed current definition)
628                }
629
630                price_increment = change
631                    .price_increment
632                    .map_or(price_increment, parse_price_increment);
633                size_increment = change
634                    .amount_increment
635                    .map_or(size_increment, parse_size_increment);
636                multiplier = match change.contract_multiplier {
637                    Some(value) => Some(Quantity::from(value.to_string())),
638                    None => multiplier,
639                };
640                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
641                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
642
643                // Get the timestamp for when the change occurred
644                ts_event = if i == sorted_changes.len() - 1 {
645                    UnixNanos::from(info.available_since)
646                } else {
647                    UnixNanos::from(change.until)
648                };
649
650                instruments.push(create_crypto_option(
651                    info,
652                    instrument_id,
653                    raw_symbol,
654                    activation,
655                    expiration,
656                    price_increment,
657                    size_increment,
658                    multiplier,
659                    margin_init,
660                    margin_maint,
661                    maker_fee,
662                    taker_fee,
663                    ts_event,
664                    ts_init.unwrap_or(ts_event),
665                )?);
666            }
667
668            // Sort in ascending (chronological) order
669            instruments.reverse();
670        }
671    }
672
673    Ok(instruments)
674}
675
676/// Parses the price increment from the given `value`.
677fn parse_price_increment(value: f64) -> Price {
678    Price::from(value.to_string())
679}
680
681/// Parses the size increment from the given `value`.
682fn parse_size_increment(value: f64) -> Quantity {
683    Quantity::from(value.to_string())
684}
685
686/// Parses the spot size increment from the given `value`.
687fn parse_spot_size_increment(value: f64, currency: Currency) -> Quantity {
688    if value == 0.0 {
689        let exponent = -i32::from(currency.precision);
690        Quantity::from(format!("{}", 10.0_f64.powi(exponent)))
691    } else {
692        Quantity::from(value.to_string())
693    }
694}
695
696/// Parses the multiplier from the given `value`.
697fn parse_multiplier(value: Option<f64>) -> Option<Quantity> {
698    value.map(|x| Quantity::from(x.to_string()))
699}
700
701/// Parses the fee rate from the given `value`.
702/// Returns zero for invalid f64 values (NaN, infinity).
703fn parse_fee_rate(value: f64) -> Decimal {
704    Decimal::try_from(value).unwrap_or_else(|e| {
705        log::warn!("Invalid fee rate value {value}: {e}, defaulting to zero");
706        Decimal::ZERO
707    })
708}
709
710/// Parses the given RFC 3339 datetime string (UTC) into a `UnixNanos` timestamp.
711/// If `value` is `None`, then defaults to the UNIX epoch (0 nanoseconds).
712/// Timestamps before UNIX epoch (negative values) default to 0.
713fn parse_datetime_to_unix_nanos(value: Option<Timestamp>) -> UnixNanos {
714    value
715        .map(|dt| match u64::try_from(dt.as_nanosecond()) {
716            Ok(nanos) => UnixNanos::from(nanos),
717            Err(_) => {
718                log::warn!("Timestamp {dt} is outside the UnixNanos range, defaulting to 0");
719                UnixNanos::default()
720            }
721        })
722        .unwrap_or_default()
723}
724
725/// Parses the settlement currency for the given Tardis instrument definition.
726#[must_use]
727pub fn parse_settlement_currency(info: &TardisInstrumentInfo, is_inverse: bool) -> String {
728    info.settlement_currency
729        .unwrap_or({
730            if is_inverse {
731                info.base_currency
732            } else {
733                info.quote_currency
734            }
735        })
736        .to_uppercase()
737}
738
739#[cfg(test)]
740mod tests {
741    use nautilus_model::{identifiers::InstrumentId, instruments::Instrument};
742    use rstest::rstest;
743
744    use super::*;
745    use crate::common::testing::load_test_json;
746
747    #[rstest]
748    fn test_parse_instrument_spot() {
749        let json_data = load_test_json("instrument_spot.json");
750        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
751
752        let instruments = parse_instrument_any(&info, None, None, false);
753        let inst0 = instruments[0].clone();
754        let inst1 = instruments[1].clone();
755
756        assert_eq!(inst0.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
757        assert_eq!(inst0.raw_symbol(), Symbol::from("BTC_USDC"));
758        assert_eq!(inst0.underlying(), None);
759        assert_eq!(inst0.base_currency(), Some(Currency::BTC()));
760        assert_eq!(inst0.quote_currency(), Currency::USDC());
761        assert_eq!(inst0.settlement_currency(), Currency::USDC());
762        assert!(!inst0.is_inverse());
763        assert_eq!(inst0.price_precision(), 2);
764        assert_eq!(inst0.size_precision(), 4);
765        assert_eq!(inst0.price_increment(), Price::from("0.01"));
766        assert_eq!(inst0.size_increment(), Quantity::from("0.0001"));
767        assert_eq!(inst0.multiplier(), Quantity::from(1));
768        assert_eq!(inst0.activation_ns(), None);
769        assert_eq!(inst0.expiration_ns(), None);
770        assert_eq!(inst0.lot_size(), Some(Quantity::from("0.0001")));
771        assert_eq!(inst0.min_quantity(), Some(Quantity::from("0.0001")));
772        assert_eq!(inst0.max_quantity(), None);
773        assert_eq!(inst0.min_notional(), None);
774        assert_eq!(inst0.max_notional(), None);
775        assert_eq!(inst0.maker_fee(), dec!(0));
776        assert_eq!(inst0.taker_fee(), dec!(0));
777        assert_eq!(inst0.ts_event().to_rfc3339(), "2023-04-24T00:00:00+00:00");
778        assert_eq!(inst0.ts_init().to_rfc3339(), "2023-04-24T00:00:00+00:00");
779
780        assert_eq!(inst1.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
781        assert_eq!(inst1.raw_symbol(), Symbol::from("BTC_USDC"));
782        assert_eq!(inst1.underlying(), None);
783        assert_eq!(inst1.base_currency(), Some(Currency::BTC()));
784        assert_eq!(inst1.quote_currency(), Currency::USDC());
785        assert_eq!(inst1.settlement_currency(), Currency::USDC());
786        assert!(!inst1.is_inverse());
787        assert_eq!(inst1.price_precision(), 0); // Changed
788        assert_eq!(inst1.size_precision(), 4);
789        assert_eq!(inst1.price_increment(), Price::from("1")); // <-- Changed
790        assert_eq!(inst1.size_increment(), Quantity::from("0.0001"));
791        assert_eq!(inst1.multiplier(), Quantity::from(1));
792        assert_eq!(inst1.activation_ns(), None);
793        assert_eq!(inst1.expiration_ns(), None);
794        assert_eq!(inst1.lot_size(), Some(Quantity::from("0.0001")));
795        assert_eq!(inst1.min_quantity(), Some(Quantity::from("0.0001")));
796        assert_eq!(inst1.max_quantity(), None);
797        assert_eq!(inst1.min_notional(), None);
798        assert_eq!(inst1.max_notional(), None);
799        assert_eq!(inst1.maker_fee(), dec!(0));
800        assert_eq!(inst1.taker_fee(), dec!(0));
801        assert_eq!(inst1.ts_event().to_rfc3339(), "2024-04-02T12:10:00+00:00");
802        assert_eq!(inst1.ts_init().to_rfc3339(), "2024-04-02T12:10:00+00:00");
803    }
804
805    #[rstest]
806    fn test_parse_instrument_perpetual() {
807        let json_data = load_test_json("instrument_perpetual.json");
808        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
809
810        let effective = UnixNanos::from("2020-08-01T08:00:00+00:00");
811        let instrument =
812            parse_instrument_any(&info, Some(effective), Some(UnixNanos::default()), false)
813                .first()
814                .unwrap()
815                .clone();
816
817        assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
818        assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
819        assert_eq!(instrument.underlying(), None);
820        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
821        assert_eq!(instrument.quote_currency(), Currency::USD());
822        assert_eq!(instrument.settlement_currency(), Currency::BTC());
823        assert!(instrument.is_inverse());
824        assert_eq!(instrument.price_precision(), 1);
825        assert_eq!(instrument.size_precision(), 0);
826        assert_eq!(instrument.price_increment(), Price::from("0.5"));
827        assert_eq!(instrument.size_increment(), Quantity::from(1));
828        assert_eq!(instrument.multiplier(), Quantity::from(1));
829        assert_eq!(instrument.activation_ns(), None);
830        assert_eq!(instrument.expiration_ns(), None);
831        assert_eq!(instrument.lot_size(), Some(Quantity::from(1)));
832        assert_eq!(instrument.min_quantity(), Some(Quantity::from(100)));
833        assert_eq!(instrument.max_quantity(), None);
834        assert_eq!(instrument.min_notional(), None);
835        assert_eq!(instrument.max_notional(), None);
836        assert_eq!(instrument.maker_fee(), dec!(0.00050));
837        assert_eq!(instrument.taker_fee(), dec!(0.00050));
838    }
839
840    #[rstest]
841    fn test_parse_instrument_future() {
842        let json_data = load_test_json("instrument_future.json");
843        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
844
845        let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
846            .first()
847            .unwrap()
848            .clone();
849
850        assert_eq!(instrument.id(), InstrumentId::from("BTC-14FEB25.DERIBIT"));
851        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-14FEB25"));
852        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
853        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
854        assert_eq!(instrument.quote_currency(), Currency::USD());
855        assert_eq!(instrument.settlement_currency(), Currency::BTC());
856        assert!(instrument.is_inverse());
857        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 2.5
858        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
859        assert_eq!(instrument.price_increment(), Price::from("2.5"));
860        assert_eq!(instrument.size_increment(), Quantity::from(10));
861        assert_eq!(instrument.multiplier(), Quantity::from(1));
862        assert_eq!(
863            instrument.activation_ns(),
864            Some(UnixNanos::from(1_738_281_600_000_000_000))
865        );
866        assert_eq!(
867            instrument.expiration_ns(),
868            Some(UnixNanos::from(1_739_520_000_000_000_000))
869        );
870        assert_eq!(instrument.lot_size(), Some(Quantity::from(10)));
871        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
872        assert_eq!(instrument.max_quantity(), None);
873        assert_eq!(instrument.min_notional(), None);
874        assert_eq!(instrument.max_notional(), None);
875        assert_eq!(instrument.maker_fee(), dec!(-0.0001));
876        assert_eq!(instrument.taker_fee(), dec!(0.0005));
877    }
878
879    #[rstest]
880    fn test_parse_instrument_perpetual_current() {
881        let json_data = load_test_json("instrument_perpetual.json");
882        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
883
884        let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
885            .last()
886            .unwrap()
887            .clone();
888
889        assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
890        assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
891        assert_eq!(instrument.size_increment(), Quantity::from(100));
892        assert_eq!(instrument.lot_size(), Some(Quantity::from(100)));
893        assert_eq!(instrument.min_quantity(), Some(Quantity::from(100)));
894    }
895
896    #[rstest]
897    fn test_parse_instrument_combo() {
898        let json_data = load_test_json("instrument_combo.json");
899        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
900
901        let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
902            .first()
903            .unwrap()
904            .clone();
905
906        assert_eq!(
907            instrument.id(),
908            InstrumentId::from("BTC-FS-28MAR25_PERP.DERIBIT")
909        );
910        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-FS-28MAR25_PERP"));
911        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
912        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
913        assert_eq!(instrument.quote_currency(), Currency::USD());
914        assert_eq!(instrument.settlement_currency(), Currency::BTC());
915        assert!(instrument.is_inverse());
916        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 0.5
917        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
918        assert_eq!(instrument.price_increment(), Price::from("0.5"));
919        assert_eq!(instrument.size_increment(), Quantity::from(10));
920        assert_eq!(instrument.multiplier(), Quantity::from(1));
921        assert_eq!(
922            instrument.activation_ns(),
923            Some(UnixNanos::from(1_711_670_400_000_000_000))
924        );
925        assert_eq!(
926            instrument.expiration_ns(),
927            Some(UnixNanos::from(1_743_148_800_000_000_000))
928        );
929        assert_eq!(instrument.lot_size(), Some(Quantity::from(10)));
930        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
931        assert_eq!(instrument.max_quantity(), None);
932        assert_eq!(instrument.min_notional(), None);
933        assert_eq!(instrument.max_notional(), None);
934        assert_eq!(instrument.maker_fee(), dec!(0));
935        assert_eq!(instrument.taker_fee(), dec!(0));
936    }
937
938    #[rstest]
939    fn test_parse_instrument_option() {
940        let json_data = load_test_json("instrument_option.json");
941        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
942
943        let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
944            .first()
945            .unwrap()
946            .clone();
947
948        assert_eq!(
949            instrument.id(),
950            InstrumentId::from("BTC-25APR25-200000-P.DERIBIT")
951        );
952        assert_eq!(
953            instrument.raw_symbol(),
954            Symbol::from("BTC-25APR25-200000-P")
955        );
956        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
957        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
958        assert_eq!(instrument.quote_currency(), Currency::BTC());
959        assert_eq!(instrument.settlement_currency(), Currency::BTC());
960        assert!(instrument.is_inverse());
961        assert_eq!(instrument.price_precision(), 4);
962        assert_eq!(instrument.size_precision(), 1); // from amountIncrement 0.1
963        assert_eq!(instrument.price_increment(), Price::from("0.0001"));
964        assert_eq!(instrument.size_increment(), Quantity::from("0.1"));
965        assert_eq!(instrument.multiplier(), Quantity::from(1));
966        assert_eq!(
967            instrument.activation_ns(),
968            Some(UnixNanos::from(1_738_281_600_000_000_000))
969        );
970        assert_eq!(
971            instrument.expiration_ns(),
972            Some(UnixNanos::from(1_745_568_000_000_000_000))
973        );
974        assert_eq!(instrument.lot_size(), Some(Quantity::from("0.1")));
975        assert_eq!(instrument.min_quantity(), Some(Quantity::from("0.1")));
976        assert_eq!(instrument.max_quantity(), None);
977        assert_eq!(instrument.min_notional(), None);
978        assert_eq!(instrument.max_notional(), None);
979        assert_eq!(instrument.maker_fee(), dec!(0.0003));
980        assert_eq!(instrument.taker_fee(), dec!(0.0003));
981    }
982
983    #[rstest]
984    fn test_parse_instrument_info_populated() {
985        let json_data = load_test_json("instrument_perpetual.json");
986        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
987
988        let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
989            .last()
990            .unwrap()
991            .clone();
992
993        let InstrumentAny::CryptoPerpetual(perp) = instrument else {
994            panic!("Expected CryptoPerpetual variant");
995        };
996
997        let info_params = perp.info.expect("info should be populated");
998
999        // Modeled fields present
1000        assert!(info_params.get("exchange").is_some());
1001        assert!(info_params.get("baseCurrency").is_some());
1002        assert!(info_params.get("quoteCurrency").is_some());
1003
1004        // Extra (unmodeled) fields preserved via #[serde(flatten)]
1005        assert_eq!(info_params.get_str("datasetId"), Some("XBTUSD"),);
1006        assert_eq!(
1007            info_params.get_str("contractType"),
1008            Some("inverse_perpetual"),
1009        );
1010        assert_eq!(info_params.get_str("underlyingIndex"), Some(".BXBT"),);
1011
1012        // Absent optional fields omitted (not null)
1013        assert!(info_params.get("listing").is_none());
1014        assert!(info_params.get("expiry").is_none());
1015    }
1016
1017    #[rstest]
1018    fn test_parse_okex_xperp_instrument() {
1019        let json_data = load_test_json("okex_futures_xperp_instrument.json");
1020        let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
1021
1022        let instruments = parse_instrument_any(&info, None, Some(UnixNanos::default()), false);
1023        let instrument = instruments.first().unwrap().clone();
1024
1025        assert_eq!(instruments.len(), 1);
1026        assert_eq!(info.instrument_type, TardisInstrumentType::Future);
1027        assert_eq!(
1028            instrument.id(),
1029            InstrumentId::from("BTC-USD_UM_XPERP-310404.OKEX")
1030        );
1031        assert_eq!(
1032            instrument.raw_symbol(),
1033            Symbol::from("BTC-USD_UM_XPERP-310404")
1034        );
1035        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
1036        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
1037        assert_eq!(instrument.quote_currency().code.as_str(), "USD_UM_XPERP");
1038        assert_eq!(
1039            instrument.settlement_currency().code.as_str(),
1040            "USD_UM_XPERP"
1041        );
1042        assert!(!instrument.is_inverse());
1043        assert_eq!(instrument.price_precision(), 1);
1044        assert_eq!(instrument.size_precision(), 0);
1045        assert_eq!(instrument.price_increment(), Price::from("0.1"));
1046        assert_eq!(instrument.size_increment(), Quantity::from(1));
1047        assert_eq!(instrument.multiplier(), Quantity::from("0.0001"));
1048        assert_eq!(
1049            instrument.activation_ns(),
1050            Some(UnixNanos::from("2026-03-30T00:00:00Z"))
1051        );
1052        assert_eq!(
1053            instrument.expiration_ns(),
1054            Some(UnixNanos::from("2031-04-04T08:00:00Z"))
1055        );
1056        assert_eq!(instrument.lot_size(), Some(Quantity::from(1)));
1057        assert_eq!(instrument.min_quantity(), Some(Quantity::from(1)));
1058        assert_eq!(instrument.max_quantity(), None);
1059        assert_eq!(instrument.maker_fee(), dec!(0.0002));
1060        assert_eq!(instrument.taker_fee(), dec!(0.0005));
1061        assert_eq!(
1062            instrument.ts_event(),
1063            UnixNanos::from("2026-03-30T00:00:00Z")
1064        );
1065
1066        let InstrumentAny::CryptoFuture(future) = instrument else {
1067            panic!("Expected CryptoFuture variant");
1068        };
1069
1070        let info_params = future.info.expect("info should be populated");
1071
1072        // X-Perp contract metadata added by tardis-node 17.2.0 is unmodeled and preserved
1073        assert_eq!(info_params.get_str("contractType"), Some("linear_xperp"));
1074        assert_eq!(info_params.get_str("underlyingType"), Some("native"));
1075        assert_eq!(info_params.get_str("underlyingIndex"), Some("BTC-USD"));
1076    }
1077}