nautilus_tardis/http/
parse.rs

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