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