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::{Price, Quantity},
24};
25use rust_decimal::Decimal;
26use rust_decimal_macros::dec;
27
28use super::{
29    instruments::{
30        create_crypto_future, create_crypto_perpetual, create_currency_pair, create_option_contract,
31    },
32    models::InstrumentInfo,
33};
34use crate::{
35    enums::InstrumentType,
36    parse::{normalize_instrument_id, parse_instrument_id},
37};
38
39#[must_use]
40pub fn parse_instrument_any(
41    info: InstrumentInfo,
42    effective: Option<UnixNanos>,
43    ts_init: Option<UnixNanos>,
44    normalize_symbols: bool,
45) -> Vec<InstrumentAny> {
46    match info.instrument_type {
47        InstrumentType::Spot => parse_spot_instrument(info, effective, ts_init, normalize_symbols),
48        InstrumentType::Perpetual => {
49            parse_perp_instrument(info, effective, ts_init, normalize_symbols)
50        }
51        InstrumentType::Future | InstrumentType::Combo => {
52            parse_future_instrument(info, effective, ts_init, normalize_symbols)
53        }
54        InstrumentType::Option => {
55            parse_option_instrument(info, effective, ts_init, normalize_symbols)
56        }
57    }
58}
59
60fn parse_spot_instrument(
61    info: InstrumentInfo,
62    effective: Option<UnixNanos>,
63    ts_init: Option<UnixNanos>,
64    normalize_symbols: bool,
65) -> Vec<InstrumentAny> {
66    let instrument_id = if normalize_symbols {
67        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
68    } else {
69        parse_instrument_id(&info.exchange, info.id)
70    };
71    let raw_symbol = Symbol::new(info.id);
72    let margin_init = dec!(0); // TBD
73    let margin_maint = dec!(0); // TBD
74    let maker_fee =
75        Decimal::from_str(info.maker_fee.to_string().as_str()).expect("Invalid decimal value");
76    let taker_fee =
77        Decimal::from_str(info.taker_fee.to_string().as_str()).expect("Invalid decimal value");
78
79    let mut price_increment = get_price_increment(info.price_increment);
80    let mut size_increment = get_size_increment(info.amount_increment);
81    let mut ts_event = UnixNanos::from(info.available_since);
82
83    let mut instruments: Vec<InstrumentAny> = Vec::new();
84
85    instruments.push(create_currency_pair(
86        &info,
87        instrument_id,
88        raw_symbol,
89        price_increment,
90        size_increment,
91        margin_init,
92        margin_maint,
93        maker_fee,
94        taker_fee,
95        ts_event,
96        ts_init.unwrap_or(ts_event),
97    ));
98
99    if let Some(changes) = &info.changes {
100        for change in changes {
101            price_increment = match change.price_increment {
102                Some(value) => get_price_increment(value),
103                None => price_increment,
104            };
105            size_increment = match change.amount_increment {
106                Some(value) => get_size_increment(value),
107                None => size_increment,
108            };
109            ts_event = UnixNanos::from(change.until);
110
111            instruments.push(create_currency_pair(
112                &info,
113                instrument_id,
114                raw_symbol,
115                price_increment,
116                size_increment,
117                margin_init,
118                margin_maint,
119                maker_fee,
120                taker_fee,
121                ts_event,
122                ts_init.unwrap_or(ts_event),
123            ));
124        }
125    }
126
127    if let Some(effective) = effective {
128        // Retain instruments up to and including the effective time
129        instruments.retain(|i| i.ts_event() <= effective);
130        if instruments.is_empty() {
131            return Vec::new();
132        }
133        // Sort descending by ts_event to keep most recent
134        instruments.sort_by_key(|i| std::cmp::Reverse(i.ts_event()));
135        // Keep only most recent version at or before effective time
136        instruments.truncate(1);
137    }
138
139    instruments
140}
141
142fn parse_perp_instrument(
143    info: InstrumentInfo,
144    effective: Option<UnixNanos>,
145    ts_init: Option<UnixNanos>,
146    normalize_symbols: bool,
147) -> Vec<InstrumentAny> {
148    let instrument_id = if normalize_symbols {
149        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
150    } else {
151        parse_instrument_id(&info.exchange, info.id)
152    };
153    let raw_symbol = Symbol::new(info.id);
154    let margin_init = dec!(0); // TBD
155    let margin_maint = dec!(0); // TBD
156    let maker_fee =
157        Decimal::from_str(info.maker_fee.to_string().as_str()).expect("Invalid decimal value");
158    let taker_fee =
159        Decimal::from_str(info.taker_fee.to_string().as_str()).expect("Invalid decimal value");
160
161    let mut price_increment = get_price_increment(info.price_increment);
162    let mut size_increment = get_size_increment(info.amount_increment);
163    let mut multiplier = get_multiplier(info.contract_multiplier);
164    let mut ts_event = UnixNanos::from(info.available_since);
165
166    let mut instruments = Vec::new();
167
168    instruments.push(create_crypto_perpetual(
169        &info,
170        instrument_id,
171        raw_symbol,
172        price_increment,
173        size_increment,
174        multiplier,
175        margin_init,
176        margin_maint,
177        maker_fee,
178        taker_fee,
179        ts_event,
180        ts_init.unwrap_or(ts_event),
181    ));
182
183    if let Some(changes) = &info.changes {
184        for change in changes {
185            price_increment = match change.price_increment {
186                Some(value) => get_price_increment(value),
187                None => price_increment,
188            };
189            size_increment = match change.amount_increment {
190                Some(value) => get_size_increment(value),
191                None => size_increment,
192            };
193            multiplier = match change.contract_multiplier {
194                Some(value) => Some(Quantity::from(value.to_string())),
195                None => multiplier,
196            };
197            ts_event = UnixNanos::from(change.until);
198
199            instruments.push(create_crypto_perpetual(
200                &info,
201                instrument_id,
202                raw_symbol,
203                price_increment,
204                size_increment,
205                multiplier,
206                margin_init,
207                margin_maint,
208                maker_fee,
209                taker_fee,
210                ts_event,
211                ts_init.unwrap_or(ts_event),
212            ));
213        }
214    }
215
216    if let Some(effective) = effective {
217        // Retain instruments up to and including the effective time
218        instruments.retain(|i| i.ts_event() <= effective);
219        if instruments.is_empty() {
220            return Vec::new();
221        }
222        // Sort descending by ts_event to keep most recent
223        instruments.sort_by_key(|i| std::cmp::Reverse(i.ts_event()));
224        // Keep only most recent version at or before effective time
225        instruments.truncate(1);
226    }
227
228    instruments
229}
230
231fn parse_future_instrument(
232    info: InstrumentInfo,
233    effective: Option<UnixNanos>,
234    ts_init: Option<UnixNanos>,
235    normalize_symbols: bool,
236) -> Vec<InstrumentAny> {
237    let instrument_id = if normalize_symbols {
238        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
239    } else {
240        parse_instrument_id(&info.exchange, info.id)
241    };
242    let raw_symbol = Symbol::new(info.id);
243    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
244    let expiration = parse_datetime_to_unix_nanos(info.expiry);
245    let margin_init = dec!(0); // TBD
246    let margin_maint = dec!(0); // TBD
247    let maker_fee =
248        Decimal::from_str(info.maker_fee.to_string().as_str()).expect("Invalid decimal value");
249    let taker_fee =
250        Decimal::from_str(info.taker_fee.to_string().as_str()).expect("Invalid decimal value");
251
252    let mut price_increment = get_price_increment(info.price_increment);
253    let mut size_increment = get_size_increment(info.amount_increment);
254    let mut multiplier = get_multiplier(info.contract_multiplier);
255    let mut ts_event = UnixNanos::from(info.available_since);
256
257    let mut instruments = Vec::new();
258
259    instruments.push(create_crypto_future(
260        &info,
261        instrument_id,
262        raw_symbol,
263        activation,
264        expiration,
265        price_increment,
266        size_increment,
267        multiplier,
268        margin_init,
269        margin_maint,
270        maker_fee,
271        taker_fee,
272        ts_event,
273        ts_init.unwrap_or(ts_event),
274    ));
275
276    if let Some(changes) = &info.changes {
277        for change in changes {
278            price_increment = match change.price_increment {
279                Some(value) => get_price_increment(value),
280                None => price_increment,
281            };
282            size_increment = match change.amount_increment {
283                Some(value) => get_size_increment(value),
284                None => size_increment,
285            };
286            multiplier = match change.contract_multiplier {
287                Some(value) => Some(Quantity::from(value.to_string())),
288                None => multiplier,
289            };
290            ts_event = UnixNanos::from(change.until);
291
292            instruments.push(create_crypto_future(
293                &info,
294                instrument_id,
295                raw_symbol,
296                activation,
297                expiration,
298                price_increment,
299                size_increment,
300                multiplier,
301                margin_init,
302                margin_maint,
303                maker_fee,
304                taker_fee,
305                ts_event,
306                ts_init.unwrap_or(ts_event),
307            ));
308        }
309    }
310
311    if let Some(effective) = effective {
312        // Retain instruments up to and including the effective time
313        instruments.retain(|i| i.ts_event() <= effective);
314        if instruments.is_empty() {
315            return Vec::new();
316        }
317        // Sort descending by ts_event to keep most recent
318        instruments.sort_by_key(|i| std::cmp::Reverse(i.ts_event()));
319        // Keep only most recent version at or before effective time
320        instruments.truncate(1);
321    }
322
323    instruments
324}
325
326fn parse_option_instrument(
327    info: InstrumentInfo,
328    effective: Option<UnixNanos>,
329    ts_init: Option<UnixNanos>,
330    normalize_symbols: bool,
331) -> Vec<InstrumentAny> {
332    let instrument_id = if normalize_symbols {
333        normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
334    } else {
335        parse_instrument_id(&info.exchange, info.id)
336    };
337    let raw_symbol = Symbol::new(info.id);
338    let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
339    let expiration = parse_datetime_to_unix_nanos(info.expiry);
340    let margin_init = dec!(0); // TBD
341    let margin_maint = dec!(0); // TBD
342    let maker_fee =
343        Decimal::from_str(info.maker_fee.to_string().as_str()).expect("Invalid decimal value");
344    let taker_fee =
345        Decimal::from_str(info.taker_fee.to_string().as_str()).expect("Invalid decimal value");
346
347    let mut price_increment = get_price_increment(info.price_increment);
348    let mut multiplier = get_multiplier(info.contract_multiplier);
349    let mut ts_event = UnixNanos::from(info.available_since);
350
351    let mut instruments = Vec::new();
352
353    instruments.push(create_option_contract(
354        &info,
355        instrument_id,
356        raw_symbol,
357        activation,
358        expiration,
359        price_increment,
360        multiplier,
361        margin_init,
362        margin_maint,
363        maker_fee,
364        taker_fee,
365        ts_event,
366        ts_init.unwrap_or(ts_event),
367    ));
368
369    if let Some(changes) = &info.changes {
370        for change in changes {
371            price_increment = match change.price_increment {
372                Some(value) => get_price_increment(value),
373                None => price_increment,
374            };
375
376            multiplier = match change.contract_multiplier {
377                Some(value) => Some(Quantity::from(value.to_string())),
378                None => multiplier,
379            };
380            ts_event = UnixNanos::from(change.until);
381
382            instruments.push(create_option_contract(
383                &info,
384                instrument_id,
385                raw_symbol,
386                activation,
387                expiration,
388                price_increment,
389                multiplier,
390                margin_init,
391                margin_maint,
392                maker_fee,
393                taker_fee,
394                ts_event,
395                ts_init.unwrap_or(ts_event),
396            ));
397        }
398    }
399
400    if let Some(effective) = effective {
401        // Retain instruments up to and including the effective time
402        instruments.retain(|i| i.ts_event() <= effective);
403        if instruments.is_empty() {
404            return Vec::new();
405        }
406        // Sort descending by ts_event to keep most recent
407        instruments.sort_by_key(|i| std::cmp::Reverse(i.ts_event()));
408        // Keep only most recent version at or before effective time
409        instruments.truncate(1);
410    }
411
412    instruments
413}
414
415/// Returns the price increment from the given `value`.
416fn get_price_increment(value: f64) -> Price {
417    Price::from(value.to_string())
418}
419
420/// Returns the size increment from the given `value`.
421fn get_size_increment(value: f64) -> Quantity {
422    Quantity::from(value.to_string())
423}
424
425fn get_multiplier(value: Option<f64>) -> Option<Quantity> {
426    value.map(|x| Quantity::from(x.to_string()))
427}
428
429/// Parses the given RFC 3339 datetime string (UTC) into a `UnixNanos` timestamp.
430/// If `value` is `None`, then defaults to the UNIX epoch (0 nanoseconds).
431fn parse_datetime_to_unix_nanos(value: Option<DateTime<Utc>>) -> UnixNanos {
432    value
433        .map(|dt| UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or(0) as u64))
434        .unwrap_or_default()
435}
436
437////////////////////////////////////////////////////////////////////////////////
438// Tests
439////////////////////////////////////////////////////////////////////////////////
440#[cfg(test)]
441mod tests {
442    use nautilus_model::{identifiers::InstrumentId, types::Currency};
443    use rstest::rstest;
444
445    use super::*;
446    use crate::tests::load_test_json;
447
448    #[rstest]
449    fn test_parse_instrument_spot() {
450        let json_data = load_test_json("instrument_spot.json");
451        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
452
453        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
454            .first()
455            .unwrap()
456            .clone();
457
458        assert_eq!(instrument.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
459        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC_USDC"));
460        assert_eq!(instrument.underlying(), None);
461        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
462        assert_eq!(instrument.quote_currency(), Currency::USDC());
463        assert_eq!(instrument.settlement_currency(), Currency::USDC());
464        assert!(!instrument.is_inverse());
465        assert_eq!(instrument.price_precision(), 0);
466        assert_eq!(instrument.size_precision(), 4);
467        assert_eq!(instrument.price_increment(), Price::from("1"));
468        assert_eq!(instrument.size_increment(), Quantity::from("0.0001"));
469        assert_eq!(instrument.multiplier(), Quantity::from(1));
470        assert_eq!(instrument.activation_ns(), None);
471        assert_eq!(instrument.expiration_ns(), None);
472        assert_eq!(instrument.min_quantity(), Some(Quantity::from("0.0001")));
473        assert_eq!(instrument.max_quantity(), None);
474        assert_eq!(instrument.min_notional(), None);
475        assert_eq!(instrument.max_notional(), None);
476        assert_eq!(instrument.maker_fee(), dec!(0));
477        assert_eq!(instrument.taker_fee(), dec!(0));
478    }
479
480    #[rstest]
481    fn test_parse_instrument_perpetual() {
482        let json_data = load_test_json("instrument_perpetual.json");
483        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
484
485        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
486            .first()
487            .unwrap()
488            .clone();
489
490        assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
491        assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
492        assert_eq!(instrument.underlying(), None);
493        assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
494        assert_eq!(instrument.quote_currency(), Currency::USD());
495        assert_eq!(instrument.settlement_currency(), Currency::USD());
496        assert!(instrument.is_inverse());
497        assert_eq!(instrument.price_precision(), 1);
498        assert_eq!(instrument.size_precision(), 0);
499        assert_eq!(instrument.price_increment(), Price::from("0.5"));
500        assert_eq!(instrument.size_increment(), Quantity::from(1));
501        assert_eq!(instrument.multiplier(), Quantity::from(1));
502        assert_eq!(instrument.activation_ns(), None);
503        assert_eq!(instrument.expiration_ns(), None);
504        assert_eq!(instrument.min_quantity(), Some(Quantity::from(1)));
505        assert_eq!(instrument.max_quantity(), None);
506        assert_eq!(instrument.min_notional(), None);
507        assert_eq!(instrument.max_notional(), None);
508        assert_eq!(instrument.maker_fee(), dec!(-0.00025));
509        assert_eq!(instrument.taker_fee(), dec!(0.00075));
510    }
511
512    #[rstest]
513    fn test_parse_instrument_future() {
514        let json_data = load_test_json("instrument_future.json");
515        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
516
517        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
518            .first()
519            .unwrap()
520            .clone();
521
522        assert_eq!(instrument.id(), InstrumentId::from("BTC-14FEB25.DERIBIT"));
523        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-14FEB25"));
524        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
525        assert_eq!(instrument.base_currency(), None);
526        assert_eq!(instrument.quote_currency(), Currency::USD());
527        assert_eq!(instrument.settlement_currency(), Currency::BTC());
528        assert!(instrument.is_inverse());
529        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 2.5
530        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
531        assert_eq!(instrument.price_increment(), Price::from("2.5"));
532        assert_eq!(instrument.size_increment(), Quantity::from(10));
533        assert_eq!(instrument.multiplier(), Quantity::from(1));
534        assert_eq!(
535            instrument.activation_ns(),
536            Some(UnixNanos::from(1738281600000000000))
537        );
538        assert_eq!(
539            instrument.expiration_ns(),
540            Some(UnixNanos::from(1739520000000000000))
541        );
542        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
543        assert_eq!(instrument.max_quantity(), None);
544        assert_eq!(instrument.min_notional(), None);
545        assert_eq!(instrument.max_notional(), None);
546        assert_eq!(instrument.maker_fee(), dec!(0));
547        assert_eq!(instrument.taker_fee(), dec!(0));
548    }
549
550    #[rstest]
551    fn test_parse_instrument_combo() {
552        let json_data = load_test_json("instrument_combo.json");
553        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
554
555        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
556            .first()
557            .unwrap()
558            .clone();
559
560        assert_eq!(
561            instrument.id(),
562            InstrumentId::from("BTC-FS-28MAR25_PERP.DERIBIT")
563        );
564        assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-FS-28MAR25_PERP"));
565        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
566        assert_eq!(instrument.base_currency(), None);
567        assert_eq!(instrument.quote_currency(), Currency::USD());
568        assert_eq!(instrument.settlement_currency(), Currency::BTC());
569        assert!(instrument.is_inverse());
570        assert_eq!(instrument.price_precision(), 1); // from priceIncrement 0.5
571        assert_eq!(instrument.size_precision(), 0); // from amountIncrement 10
572        assert_eq!(instrument.price_increment(), Price::from("0.5"));
573        assert_eq!(instrument.size_increment(), Quantity::from(10));
574        assert_eq!(instrument.multiplier(), Quantity::from(1));
575        assert_eq!(
576            instrument.activation_ns(),
577            Some(UnixNanos::from(1711670400000000000))
578        );
579        assert_eq!(
580            instrument.expiration_ns(),
581            Some(UnixNanos::from(1743148800000000000))
582        );
583        assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
584        assert_eq!(instrument.max_quantity(), None);
585        assert_eq!(instrument.min_notional(), None);
586        assert_eq!(instrument.max_notional(), None);
587        assert_eq!(instrument.maker_fee(), dec!(0));
588        assert_eq!(instrument.taker_fee(), dec!(0));
589    }
590
591    #[rstest]
592    fn test_parse_instrument_option() {
593        let json_data = load_test_json("instrument_option.json");
594        let info: InstrumentInfo = serde_json::from_str(&json_data).unwrap();
595
596        let instrument = parse_instrument_any(info, None, Some(UnixNanos::default()), false)
597            .first()
598            .unwrap()
599            .clone();
600
601        assert_eq!(
602            instrument.id(),
603            InstrumentId::from("BTC-25APR25-200000-P.DERIBIT")
604        );
605        assert_eq!(
606            instrument.raw_symbol(),
607            Symbol::from("BTC-25APR25-200000-P")
608        );
609        assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
610        assert_eq!(instrument.base_currency(), None);
611        assert_eq!(instrument.quote_currency(), Currency::BTC());
612        assert_eq!(instrument.settlement_currency(), Currency::BTC());
613        // assert!(instrument.is_inverse());  // TODO: Implement inverse options
614        assert_eq!(instrument.price_precision(), 4);
615        // assert_eq!(instrument.size_precision(), 1); // from amountIncrement 0.1
616        assert_eq!(instrument.price_increment(), Price::from("0.0001"));
617        // assert_eq!(instrument.size_increment(), Quantity::from("0.1"));
618        assert_eq!(instrument.multiplier(), Quantity::from(1));
619        assert_eq!(
620            instrument.activation_ns(),
621            Some(UnixNanos::from(1738281600000000000))
622        );
623        assert_eq!(
624            instrument.expiration_ns(),
625            Some(UnixNanos::from(1745568000000000000))
626        );
627        assert_eq!(instrument.min_quantity(), Some(Quantity::from("0.1")));
628        assert_eq!(instrument.max_quantity(), None);
629        assert_eq!(instrument.min_notional(), None);
630        assert_eq!(instrument.max_notional(), None);
631        assert_eq!(instrument.maker_fee(), dec!(0));
632        assert_eq!(instrument.taker_fee(), dec!(0));
633    }
634}