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