nautilus_tardis/http/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 std::str::FromStr;
17
18use chrono::{DateTime, Utc};
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21    identifiers::Symbol,
22    instruments::InstrumentAny,
23    types::{Currency, Price, Quantity},
24};
25use rust_decimal::Decimal;
26use rust_decimal_macros::dec;
27
28use super::{
29    instruments::{
30        create_crypto_future, create_crypto_option, create_crypto_perpetual, create_currency_pair,
31        get_currency,
32    },
33    models::InstrumentInfo,
34};
35use crate::{
36    enums::InstrumentType,
37    parse::{normalize_instrument_id, parse_instrument_id},
38};
39
40#[must_use]
41pub fn parse_instrument_any(
42    info: InstrumentInfo,
43    effective: Option<UnixNanos>,
44    ts_init: Option<UnixNanos>,
45    normalize_symbols: bool,
46) -> Vec<InstrumentAny> {
47    match info.instrument_type {
48        InstrumentType::Spot => parse_spot_instrument(info, effective, ts_init, normalize_symbols),
49        InstrumentType::Perpetual => {
50            parse_perp_instrument(info, effective, ts_init, normalize_symbols)
51        }
52        InstrumentType::Future | InstrumentType::Combo => {
53            parse_future_instrument(info, effective, ts_init, normalize_symbols)
54        }
55        InstrumentType::Option => {
56            parse_option_instrument(info, effective, ts_init, normalize_symbols)
57        }
58    }
59}
60
61fn parse_spot_instrument(
62    info: InstrumentInfo,
63    effective: Option<UnixNanos>,
64    ts_init: Option<UnixNanos>,
65    normalize_symbols: bool,
66) -> Vec<InstrumentAny> {
67    let instrument_id = if normalize_symbols {
68        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
69    } else {
70        parse_instrument_id(&info.exchange, info.id)
71    };
72    let raw_symbol = Symbol::new(info.id);
73    let margin_init = dec!(0); // TBD
74    let margin_maint = dec!(0); // TBD
75
76    let mut price_increment = parse_price_increment(info.price_increment);
77    let base_currency = get_currency(info.base_currency.to_uppercase().as_str());
78    let mut size_increment = parse_spot_size_increment(info.amount_increment, base_currency);
79    let mut maker_fee = parse_fee_rate(info.maker_fee);
80    let mut taker_fee = parse_fee_rate(info.taker_fee);
81    let mut ts_event = info
82        .changes
83        .as_ref()
84        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
85        .unwrap_or_else(|| UnixNanos::from(info.available_since));
86
87    // Current instrument definition
88    let mut instruments = vec![create_currency_pair(
89        &info,
90        instrument_id,
91        raw_symbol,
92        price_increment,
93        size_increment,
94        margin_init,
95        margin_maint,
96        maker_fee,
97        taker_fee,
98        ts_event,
99        ts_init.unwrap_or(ts_event),
100    )];
101
102    if let Some(changes) = &info.changes {
103        // Sort changes newest to oldest
104        let mut sorted_changes = changes.clone();
105        sorted_changes.sort_by(|a, b| b.until.cmp(&a.until));
106
107        if let Some(effective_time) = effective {
108            // Apply changes where change.until >= effective_time
109            for (i, change) in sorted_changes.iter().enumerate() {
110                if change.price_increment.is_none()
111                    && change.amount_increment.is_none()
112                    && change.contract_multiplier.is_none()
113                {
114                    continue; // No changes to apply (already pushed current definition)
115                }
116
117                ts_event = UnixNanos::from(change.until);
118
119                if ts_event < effective_time {
120                    break; // Early exit since changes are sorted newest to oldest
121                } else if i == sorted_changes.len() - 1 {
122                    ts_event = UnixNanos::from(info.available_since);
123                }
124
125                price_increment = change
126                    .price_increment
127                    .map_or(price_increment, parse_price_increment);
128                size_increment = change.amount_increment.map_or(size_increment, |value| {
129                    parse_spot_size_increment(value, base_currency)
130                });
131                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
132                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
133            }
134
135            // Replace with single instrument reflecting effective state
136            instruments = vec![create_currency_pair(
137                &info,
138                instrument_id,
139                raw_symbol,
140                price_increment,
141                size_increment,
142                margin_init,
143                margin_maint,
144                maker_fee,
145                taker_fee,
146                ts_event,
147                ts_init.unwrap_or(ts_event),
148            )];
149        } else {
150            // Historical sequence with all states
151            for (i, change) in sorted_changes.iter().enumerate() {
152                if change.price_increment.is_none()
153                    && change.amount_increment.is_none()
154                    && change.contract_multiplier.is_none()
155                {
156                    continue; // No changes to apply (already pushed current definition)
157                }
158
159                price_increment = change
160                    .price_increment
161                    .map_or(price_increment, parse_price_increment);
162                size_increment = change.amount_increment.map_or(size_increment, |value| {
163                    parse_spot_size_increment(value, base_currency)
164                });
165                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
166                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
167
168                // Get the timestamp for when the change occurred
169                ts_event = if i == sorted_changes.len() - 1 {
170                    UnixNanos::from(info.available_since)
171                } else {
172                    UnixNanos::from(change.until)
173                };
174
175                instruments.push(create_currency_pair(
176                    &info,
177                    instrument_id,
178                    raw_symbol,
179                    price_increment,
180                    size_increment,
181                    margin_init,
182                    margin_maint,
183                    maker_fee,
184                    taker_fee,
185                    ts_event,
186                    ts_init.unwrap_or(ts_event),
187                ));
188            }
189
190            // Sort in ascending (chronological) order
191            instruments.reverse();
192        }
193    }
194
195    instruments
196}
197
198fn parse_perp_instrument(
199    info: InstrumentInfo,
200    effective: Option<UnixNanos>,
201    ts_init: Option<UnixNanos>,
202    normalize_symbols: bool,
203) -> Vec<InstrumentAny> {
204    let instrument_id = if normalize_symbols {
205        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
206    } else {
207        parse_instrument_id(&info.exchange, info.id)
208    };
209    let raw_symbol = Symbol::new(info.id);
210    let margin_init = dec!(0); // TBD
211    let margin_maint = dec!(0); // TBD
212
213    let mut price_increment = parse_price_increment(info.price_increment);
214    let mut size_increment = parse_size_increment(info.amount_increment);
215    let mut multiplier = parse_multiplier(info.contract_multiplier);
216    let mut maker_fee = parse_fee_rate(info.maker_fee);
217    let mut taker_fee = parse_fee_rate(info.taker_fee);
218    let mut ts_event = info
219        .changes
220        .as_ref()
221        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
222        .unwrap_or_else(|| UnixNanos::from(info.available_since));
223
224    // Current instrument definition
225    let mut instruments = vec![create_crypto_perpetual(
226        &info,
227        instrument_id,
228        raw_symbol,
229        price_increment,
230        size_increment,
231        multiplier,
232        margin_init,
233        margin_maint,
234        maker_fee,
235        taker_fee,
236        ts_event,
237        ts_init.unwrap_or(ts_event),
238    )];
239
240    if let Some(changes) = &info.changes {
241        // Sort changes newest to oldest
242        let mut sorted_changes = changes.clone();
243        sorted_changes.sort_by(|a, b| b.until.cmp(&a.until));
244
245        if let Some(effective_time) = effective {
246            // Apply changes where change.until >= effective_time
247            for (i, change) in sorted_changes.iter().enumerate() {
248                if change.price_increment.is_none()
249                    && change.amount_increment.is_none()
250                    && change.contract_multiplier.is_none()
251                {
252                    continue; // No changes to apply (already pushed current definition)
253                }
254
255                ts_event = UnixNanos::from(change.until);
256
257                if ts_event < effective_time {
258                    break; // Early exit since changes are sorted newest to oldest
259                } else if i == sorted_changes.len() - 1 {
260                    ts_event = UnixNanos::from(info.available_since);
261                }
262
263                price_increment = change
264                    .price_increment
265                    .map_or(price_increment, parse_price_increment);
266                size_increment = change
267                    .amount_increment
268                    .map_or(size_increment, parse_size_increment);
269                multiplier = match change.contract_multiplier {
270                    Some(value) => Some(Quantity::from(value.to_string())),
271                    None => multiplier,
272                };
273                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
274                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
275            }
276
277            // Replace with single instrument reflecting effective state
278            instruments = vec![create_crypto_perpetual(
279                &info,
280                instrument_id,
281                raw_symbol,
282                price_increment,
283                size_increment,
284                multiplier,
285                margin_init,
286                margin_maint,
287                maker_fee,
288                taker_fee,
289                ts_event,
290                ts_init.unwrap_or(ts_event),
291            )];
292        } else {
293            // Historical view with all states
294            for (i, change) in sorted_changes.iter().enumerate() {
295                if change.price_increment.is_none()
296                    && change.amount_increment.is_none()
297                    && change.contract_multiplier.is_none()
298                {
299                    continue; // No changes to apply (already pushed current definition)
300                }
301
302                price_increment = change
303                    .price_increment
304                    .map_or(price_increment, parse_price_increment);
305                size_increment = change
306                    .amount_increment
307                    .map_or(size_increment, parse_size_increment);
308                multiplier = match change.contract_multiplier {
309                    Some(value) => Some(Quantity::from(value.to_string())),
310                    None => multiplier,
311                };
312                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
313                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
314
315                // Get the timestamp for when the change occurred
316                ts_event = if i == sorted_changes.len() - 1 {
317                    UnixNanos::from(info.available_since)
318                } else {
319                    UnixNanos::from(change.until)
320                };
321
322                instruments.push(create_crypto_perpetual(
323                    &info,
324                    instrument_id,
325                    raw_symbol,
326                    price_increment,
327                    size_increment,
328                    multiplier,
329                    margin_init,
330                    margin_maint,
331                    maker_fee,
332                    taker_fee,
333                    ts_event,
334                    ts_init.unwrap_or(ts_event),
335                ));
336            }
337
338            // Sort in ascending (chronological) order
339            instruments.reverse();
340        }
341    }
342
343    instruments
344}
345
346fn parse_future_instrument(
347    info: InstrumentInfo,
348    effective: Option<UnixNanos>,
349    ts_init: Option<UnixNanos>,
350    normalize_symbols: bool,
351) -> Vec<InstrumentAny> {
352    let instrument_id = if normalize_symbols {
353        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
354    } else {
355        parse_instrument_id(&info.exchange, info.id)
356    };
357    let raw_symbol = Symbol::new(info.id);
358    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
359    let expiration = parse_datetime_to_unix_nanos(info.expiry);
360    let margin_init = dec!(0); // TBD
361    let margin_maint = dec!(0); // TBD
362
363    let mut price_increment = parse_price_increment(info.price_increment);
364    let mut size_increment = parse_size_increment(info.amount_increment);
365    let mut multiplier = parse_multiplier(info.contract_multiplier);
366    let mut maker_fee = parse_fee_rate(info.maker_fee);
367    let mut taker_fee = parse_fee_rate(info.taker_fee);
368    let mut ts_event = info
369        .changes
370        .as_ref()
371        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
372        .unwrap_or_else(|| UnixNanos::from(info.available_since));
373
374    // Current instrument definition
375    let mut instruments = vec![create_crypto_future(
376        &info,
377        instrument_id,
378        raw_symbol,
379        activation,
380        expiration,
381        price_increment,
382        size_increment,
383        multiplier,
384        margin_init,
385        margin_maint,
386        maker_fee,
387        taker_fee,
388        ts_event,
389        ts_init.unwrap_or(ts_event),
390    )];
391
392    if let Some(changes) = &info.changes {
393        // Sort changes newest to oldest
394        let mut sorted_changes = changes.clone();
395        sorted_changes.sort_by(|a, b| b.until.cmp(&a.until));
396
397        if let Some(effective_time) = effective {
398            // Apply changes where change.until >= effective_time
399            for (i, change) in sorted_changes.iter().enumerate() {
400                if change.price_increment.is_none()
401                    && change.amount_increment.is_none()
402                    && change.contract_multiplier.is_none()
403                {
404                    continue; // No changes to apply (already pushed current definition)
405                }
406
407                ts_event = UnixNanos::from(change.until);
408
409                if ts_event < effective_time {
410                    break; // Early exit since changes are sorted newest to oldest
411                } else if i == sorted_changes.len() - 1 {
412                    ts_event = UnixNanos::from(info.available_since);
413                }
414
415                price_increment = change
416                    .price_increment
417                    .map_or(price_increment, parse_price_increment);
418                size_increment = change
419                    .amount_increment
420                    .map_or(size_increment, parse_size_increment);
421                multiplier = match change.contract_multiplier {
422                    Some(value) => Some(Quantity::from(value.to_string())),
423                    None => multiplier,
424                };
425                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
426                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
427            }
428
429            // Replace with single instrument reflecting effective state
430            instruments = vec![create_crypto_future(
431                &info,
432                instrument_id,
433                raw_symbol,
434                activation,
435                expiration,
436                price_increment,
437                size_increment,
438                multiplier,
439                margin_init,
440                margin_maint,
441                maker_fee,
442                taker_fee,
443                ts_event,
444                ts_init.unwrap_or(ts_event),
445            )];
446        } else {
447            // Historical view with all states
448            for (i, change) in sorted_changes.iter().enumerate() {
449                if change.price_increment.is_none()
450                    && change.amount_increment.is_none()
451                    && change.contract_multiplier.is_none()
452                {
453                    continue; // No changes to apply (already pushed current definition)
454                }
455
456                price_increment = change
457                    .price_increment
458                    .map_or(price_increment, parse_price_increment);
459                size_increment = change
460                    .amount_increment
461                    .map_or(size_increment, parse_size_increment);
462                multiplier = match change.contract_multiplier {
463                    Some(value) => Some(Quantity::from(value.to_string())),
464                    None => multiplier,
465                };
466                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
467                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
468
469                // Get the timestamp for when the change occurred
470                ts_event = if i == sorted_changes.len() - 1 {
471                    UnixNanos::from(info.available_since)
472                } else {
473                    UnixNanos::from(change.until)
474                };
475
476                instruments.push(create_crypto_future(
477                    &info,
478                    instrument_id,
479                    raw_symbol,
480                    activation,
481                    expiration,
482                    price_increment,
483                    size_increment,
484                    multiplier,
485                    margin_init,
486                    margin_maint,
487                    maker_fee,
488                    taker_fee,
489                    ts_event,
490                    ts_init.unwrap_or(ts_event),
491                ));
492            }
493
494            // Sort in ascending (chronological) order
495            instruments.reverse();
496        }
497    }
498
499    instruments
500}
501
502fn parse_option_instrument(
503    info: InstrumentInfo,
504    effective: Option<UnixNanos>,
505    ts_init: Option<UnixNanos>,
506    normalize_symbols: bool,
507) -> Vec<InstrumentAny> {
508    let instrument_id = if normalize_symbols {
509        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
510    } else {
511        parse_instrument_id(&info.exchange, info.id)
512    };
513    let raw_symbol = Symbol::new(info.id);
514    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
515    let expiration = parse_datetime_to_unix_nanos(info.expiry);
516    let margin_init = dec!(0); // TBD
517    let margin_maint = dec!(0); // TBD
518
519    let mut price_increment = parse_price_increment(info.price_increment);
520    let mut size_increment = parse_size_increment(info.amount_increment);
521    let mut multiplier = parse_multiplier(info.contract_multiplier);
522    let mut maker_fee = parse_fee_rate(info.maker_fee);
523    let mut taker_fee = parse_fee_rate(info.taker_fee);
524    let mut ts_event = info
525        .changes
526        .as_ref()
527        .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
528        .unwrap_or_else(|| UnixNanos::from(info.available_since));
529
530    // Current instrument definition
531    let mut instruments = vec![create_crypto_option(
532        &info,
533        instrument_id,
534        raw_symbol,
535        activation,
536        expiration,
537        price_increment,
538        size_increment,
539        multiplier,
540        margin_init,
541        margin_maint,
542        maker_fee,
543        taker_fee,
544        ts_event,
545        ts_init.unwrap_or(ts_event),
546    )];
547
548    if let Some(changes) = &info.changes {
549        // Sort changes newest to oldest
550        let mut sorted_changes = changes.clone();
551        sorted_changes.sort_by(|a, b| b.until.cmp(&a.until));
552
553        if let Some(effective_time) = effective {
554            // Apply changes where change.until >= effective_time
555            for (i, change) in sorted_changes.iter().enumerate() {
556                if change.price_increment.is_none()
557                    && change.amount_increment.is_none()
558                    && change.contract_multiplier.is_none()
559                {
560                    continue; // No changes to apply (already pushed current definition)
561                }
562
563                ts_event = UnixNanos::from(change.until);
564
565                if ts_event < effective_time {
566                    break; // Early exit since changes are sorted newest to oldest
567                } else if i == sorted_changes.len() - 1 {
568                    ts_event = UnixNanos::from(info.available_since);
569                }
570
571                price_increment = change
572                    .price_increment
573                    .map_or(price_increment, parse_price_increment);
574                size_increment = change
575                    .amount_increment
576                    .map_or(size_increment, parse_size_increment);
577                multiplier = match change.contract_multiplier {
578                    Some(value) => Some(Quantity::from(value.to_string())),
579                    None => multiplier,
580                };
581                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
582                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
583            }
584
585            // Replace with single instrument reflecting effective state
586            instruments = vec![create_crypto_option(
587                &info,
588                instrument_id,
589                raw_symbol,
590                activation,
591                expiration,
592                price_increment,
593                size_increment,
594                multiplier,
595                margin_init,
596                margin_maint,
597                maker_fee,
598                taker_fee,
599                ts_event,
600                ts_init.unwrap_or(ts_event),
601            )];
602        } else {
603            // Historical view with all states
604            for (i, change) in sorted_changes.iter().enumerate() {
605                if change.price_increment.is_none()
606                    && change.amount_increment.is_none()
607                    && change.contract_multiplier.is_none()
608                {
609                    continue; // No changes to apply (already pushed current definition)
610                }
611
612                price_increment = change
613                    .price_increment
614                    .map_or(price_increment, parse_price_increment);
615                size_increment = change
616                    .amount_increment
617                    .map_or(size_increment, parse_size_increment);
618                multiplier = match change.contract_multiplier {
619                    Some(value) => Some(Quantity::from(value.to_string())),
620                    None => multiplier,
621                };
622                maker_fee = change.maker_fee.map_or(maker_fee, parse_fee_rate);
623                taker_fee = change.taker_fee.map_or(taker_fee, parse_fee_rate);
624
625                // Get the timestamp for when the change occurred
626                ts_event = if i == sorted_changes.len() - 1 {
627                    UnixNanos::from(info.available_since)
628                } else {
629                    UnixNanos::from(change.until)
630                };
631
632                instruments.push(create_crypto_option(
633                    &info,
634                    instrument_id,
635                    raw_symbol,
636                    activation,
637                    expiration,
638                    price_increment,
639                    size_increment,
640                    multiplier,
641                    margin_init,
642                    margin_maint,
643                    maker_fee,
644                    taker_fee,
645                    ts_event,
646                    ts_init.unwrap_or(ts_event),
647                ));
648            }
649
650            // Sort in ascending (chronological) order
651            instruments.reverse();
652        }
653    }
654
655    instruments
656}
657
658/// Parses the price increment from the given `value`.
659fn parse_price_increment(value: f64) -> Price {
660    Price::from(value.to_string())
661}
662
663/// Parses the size increment from the given `value`.
664fn parse_size_increment(value: f64) -> Quantity {
665    Quantity::from(value.to_string())
666}
667
668/// Parses the spot size increment from the given `value`.
669fn parse_spot_size_increment(value: f64, currency: Currency) -> Quantity {
670    if value == 0.0 {
671        let exponent = -i32::from(currency.precision);
672        Quantity::from(format!("{}", 10.0_f64.powi(exponent)))
673    } else {
674        Quantity::from(value.to_string())
675    }
676}
677
678/// Parses the multiplier from the given `value`.
679fn parse_multiplier(value: Option<f64>) -> Option<Quantity> {
680    value.map(|x| Quantity::from(x.to_string()))
681}
682
683/// Parses the fee rate from the given `value`.
684fn parse_fee_rate(value: f64) -> Decimal {
685    Decimal::from_str(&value.to_string()).expect("Invalid decimal value")
686}
687
688/// Parses the given RFC 3339 datetime string (UTC) into a `UnixNanos` timestamp.
689/// If `value` is `None`, then defaults to the UNIX epoch (0 nanoseconds).
690fn parse_datetime_to_unix_nanos(value: Option<DateTime<Utc>>) -> UnixNanos {
691    value
692        .map(|dt| UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or(0) as u64))
693        .unwrap_or_default()
694}
695
696/// Parses the settlement currency for the given Tardis instrument definition.
697#[must_use]
698pub fn parse_settlement_currency(info: &InstrumentInfo, is_inverse: bool) -> String {
699    info.settlement_currency
700        .unwrap_or({
701            if is_inverse {
702                info.base_currency
703            } else {
704                info.quote_currency
705            }
706        })
707        .to_uppercase()
708}
709
710////////////////////////////////////////////////////////////////////////////////
711// Tests
712////////////////////////////////////////////////////////////////////////////////
713#[cfg(test)]
714mod tests {
715    use nautilus_model::{identifiers::InstrumentId, instruments::Instrument, types::Currency};
716    use rstest::rstest;
717
718    use super::*;
719    use crate::tests::load_test_json;
720
721    #[rstest]
722    fn test_parse_instrument_spot() {
723        let json_data = load_test_json("instrument_spot.json");
724        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
725
726        let instruments = parse_instrument_any(info, None, None, false);
727        let inst0 = instruments[0].clone();
728        let inst1 = instruments[1].clone();
729
730        assert_eq!(inst0.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
731        assert_eq!(inst0.raw_symbol(), Symbol::from("BTC_USDC"));
732        assert_eq!(inst0.underlying(), None);
733        assert_eq!(inst0.base_currency(), Some(Currency::BTC()));
734        assert_eq!(inst0.quote_currency(), Currency::USDC());
735        assert_eq!(inst0.settlement_currency(), Currency::USDC());
736        assert!(!inst0.is_inverse());
737        assert_eq!(inst0.price_precision(), 2);
738        assert_eq!(inst0.size_precision(), 4);
739        assert_eq!(inst0.price_increment(), Price::from("0.01"));
740        assert_eq!(inst0.size_increment(), Quantity::from("0.0001"));
741        assert_eq!(inst0.multiplier(), Quantity::from(1));
742        assert_eq!(inst0.activation_ns(), None);
743        assert_eq!(inst0.expiration_ns(), None);
744        assert_eq!(inst0.min_quantity(), Some(Quantity::from("0.0001")));
745        assert_eq!(inst0.max_quantity(), None);
746        assert_eq!(inst0.min_notional(), None);
747        assert_eq!(inst0.max_notional(), None);
748        assert_eq!(inst0.maker_fee(), dec!(0));
749        assert_eq!(inst0.taker_fee(), dec!(0));
750        assert_eq!(inst0.ts_event().to_rfc3339(), "2023-04-24T00:00:00+00:00");
751        assert_eq!(inst0.ts_init().to_rfc3339(), "2023-04-24T00:00:00+00:00");
752
753        assert_eq!(inst1.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
754        assert_eq!(inst1.raw_symbol(), Symbol::from("BTC_USDC"));
755        assert_eq!(inst1.underlying(), None);
756        assert_eq!(inst1.base_currency(), Some(Currency::BTC()));
757        assert_eq!(inst1.quote_currency(), Currency::USDC());
758        assert_eq!(inst1.settlement_currency(), Currency::USDC());
759        assert!(!inst1.is_inverse());
760        assert_eq!(inst1.price_precision(), 0); // Changed
761        assert_eq!(inst1.size_precision(), 4);
762        assert_eq!(inst1.price_increment(), Price::from("1")); // <-- Changed
763        assert_eq!(inst1.size_increment(), Quantity::from("0.0001"));
764        assert_eq!(inst1.multiplier(), Quantity::from(1));
765        assert_eq!(inst1.activation_ns(), None);
766        assert_eq!(inst1.expiration_ns(), None);
767        assert_eq!(inst1.min_quantity(), Some(Quantity::from("0.0001")));
768        assert_eq!(inst1.max_quantity(), None);
769        assert_eq!(inst1.min_notional(), None);
770        assert_eq!(inst1.max_notional(), None);
771        assert_eq!(inst1.maker_fee(), dec!(0));
772        assert_eq!(inst1.taker_fee(), dec!(0));
773        assert_eq!(inst1.ts_event().to_rfc3339(), "2024-04-02T12:10:00+00:00");
774        assert_eq!(inst1.ts_init().to_rfc3339(), "2024-04-02T12:10:00+00:00");
775    }
776
777    #[rstest]
778    fn test_parse_instrument_perpetual() {
779        let json_data = load_test_json("instrument_perpetual.json");
780        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
781
782        let effective = UnixNanos::from("2020-08-01T08:00:00+00:00");
783        let instrument =
784            parse_instrument_any(info, Some(effective), Some(UnixNanos::default()), false)
785                .first()
786                .unwrap()
787                .clone();
788
789        assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
790        assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
791        assert_eq!(instrument.underlying(), None);
792        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
793        assert_eq!(instrument.quote_currency(), Currency::USD());
794        assert_eq!(instrument.settlement_currency(), Currency::BTC());
795        assert!(instrument.is_inverse());
796        assert_eq!(instrument.price_precision(), 1);
797        assert_eq!(instrument.size_precision(), 0);
798        assert_eq!(instrument.price_increment(), Price::from("0.5"));
799        assert_eq!(instrument.size_increment(), Quantity::from(1));
800        assert_eq!(instrument.multiplier(), Quantity::from(1));
801        assert_eq!(instrument.activation_ns(), None);
802        assert_eq!(instrument.expiration_ns(), None);
803        assert_eq!(instrument.min_quantity(), Some(Quantity::from(100)));
804        assert_eq!(instrument.max_quantity(), None);
805        assert_eq!(instrument.min_notional(), None);
806        assert_eq!(instrument.max_notional(), None);
807        assert_eq!(instrument.maker_fee(), dec!(0.00050));
808        assert_eq!(instrument.taker_fee(), dec!(0.00050));
809    }
810
811    #[rstest]
812    fn test_parse_instrument_future() {
813        let json_data = load_test_json("instrument_future.json");
814        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
815
816        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
817            .first()
818            .unwrap()
819            .clone();
820
821        assert_eq!(instrument.id(), InstrumentId::from("BTC-14FEB25.DERIBIT"));
822        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-14FEB25"));
823        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
824        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
825        assert_eq!(instrument.quote_currency(), Currency::USD());
826        assert_eq!(instrument.settlement_currency(), Currency::BTC());
827        assert!(instrument.is_inverse());
828        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 2.5
829        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
830        assert_eq!(instrument.price_increment(), Price::from("2.5"));
831        assert_eq!(instrument.size_increment(), Quantity::from(10));
832        assert_eq!(instrument.multiplier(), Quantity::from(1));
833        assert_eq!(
834            instrument.activation_ns(),
835            Some(UnixNanos::from(1_738_281_600_000_000_000))
836        );
837        assert_eq!(
838            instrument.expiration_ns(),
839            Some(UnixNanos::from(1_739_520_000_000_000_000))
840        );
841        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
842        assert_eq!(instrument.max_quantity(), None);
843        assert_eq!(instrument.min_notional(), None);
844        assert_eq!(instrument.max_notional(), None);
845        assert_eq!(instrument.maker_fee(), dec!(0));
846        assert_eq!(instrument.taker_fee(), dec!(0));
847    }
848
849    #[rstest]
850    fn test_parse_instrument_combo() {
851        let json_data = load_test_json("instrument_combo.json");
852        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
853
854        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
855            .first()
856            .unwrap()
857            .clone();
858
859        assert_eq!(
860            instrument.id(),
861            InstrumentId::from("BTC-FS-28MAR25_PERP.DERIBIT")
862        );
863        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-FS-28MAR25_PERP"));
864        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
865        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
866        assert_eq!(instrument.quote_currency(), Currency::USD());
867        assert_eq!(instrument.settlement_currency(), Currency::BTC());
868        assert!(instrument.is_inverse());
869        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 0.5
870        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
871        assert_eq!(instrument.price_increment(), Price::from("0.5"));
872        assert_eq!(instrument.size_increment(), Quantity::from(10));
873        assert_eq!(instrument.multiplier(), Quantity::from(1));
874        assert_eq!(
875            instrument.activation_ns(),
876            Some(UnixNanos::from(1_711_670_400_000_000_000))
877        );
878        assert_eq!(
879            instrument.expiration_ns(),
880            Some(UnixNanos::from(1_743_148_800_000_000_000))
881        );
882        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
883        assert_eq!(instrument.max_quantity(), None);
884        assert_eq!(instrument.min_notional(), None);
885        assert_eq!(instrument.max_notional(), None);
886        assert_eq!(instrument.maker_fee(), dec!(0));
887        assert_eq!(instrument.taker_fee(), dec!(0));
888    }
889
890    #[rstest]
891    fn test_parse_instrument_option() {
892        let json_data = load_test_json("instrument_option.json");
893        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
894
895        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
896            .first()
897            .unwrap()
898            .clone();
899
900        assert_eq!(
901            instrument.id(),
902            InstrumentId::from("BTC-25APR25-200000-P.DERIBIT")
903        );
904        assert_eq!(
905            instrument.raw_symbol(),
906            Symbol::from("BTC-25APR25-200000-P")
907        );
908        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
909        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
910        assert_eq!(instrument.quote_currency(), Currency::BTC());
911        assert_eq!(instrument.settlement_currency(), Currency::BTC());
912        assert!(!instrument.is_inverse());
913        assert_eq!(instrument.price_precision(), 4);
914        assert_eq!(instrument.size_precision(), 1); // from amountIncrement 0.1
915        assert_eq!(instrument.price_increment(), Price::from("0.0001"));
916        assert_eq!(instrument.size_increment(), Quantity::from("0.1"));
917        assert_eq!(instrument.multiplier(), Quantity::from(1));
918        assert_eq!(
919            instrument.activation_ns(),
920            Some(UnixNanos::from(1_738_281_600_000_000_000))
921        );
922        assert_eq!(
923            instrument.expiration_ns(),
924            Some(UnixNanos::from(1_745_568_000_000_000_000))
925        );
926        assert_eq!(instrument.min_quantity(), Some(Quantity::from("0.1")));
927        assert_eq!(instrument.max_quantity(), None);
928        assert_eq!(instrument.min_notional(), None);
929        assert_eq!(instrument.max_notional(), None);
930        assert_eq!(instrument.maker_fee(), dec!(0));
931        assert_eq!(instrument.taker_fee(), dec!(0));
932    }
933}