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