1use nautilus_core::{Params, UnixNanos};
17use nautilus_model::{
18 identifiers::{InstrumentId, Symbol},
19 instruments::{CryptoFuture, CryptoOption, CryptoPerpetual, CurrencyPair, InstrumentAny},
20 types::{Currency, Price, Quantity},
21};
22use rust_decimal::Decimal;
23
24use super::{models::TardisInstrumentInfo, parse::parse_settlement_currency};
25use crate::common::parse::parse_option_kind;
26
27pub(crate) fn get_currency(code: &str) -> Currency {
32 Currency::get_or_create_crypto(code)
33}
34
35fn build_info_params(info: &TardisInstrumentInfo) -> Option<Params> {
37 match serde_json::to_value(info) {
38 Ok(value) => match serde_json::from_value(value) {
39 Ok(params) => Some(params),
40 Err(e) => {
41 log::warn!("Failed to convert instrument info to Params: {e}");
42 None
43 }
44 },
45 Err(e) => {
46 log::warn!("Failed to serialize instrument info: {e}");
47 None
48 }
49 }
50}
51
52#[expect(clippy::too_many_arguments)]
58#[must_use]
59pub fn create_currency_pair(
60 info: &TardisInstrumentInfo,
61 instrument_id: InstrumentId,
62 raw_symbol: Symbol,
63 price_increment: Price,
64 size_increment: Quantity,
65 multiplier: Option<Quantity>,
66 margin_init: Decimal,
67 margin_maint: Decimal,
68 maker_fee: Decimal,
69 taker_fee: Decimal,
70 ts_event: UnixNanos,
71 ts_init: UnixNanos,
72) -> InstrumentAny {
73 InstrumentAny::CurrencyPair(
74 CurrencyPair::builder()
75 .instrument_id(instrument_id)
76 .raw_symbol(raw_symbol)
77 .base_currency(get_currency(info.base_currency.to_uppercase().as_str()))
78 .quote_currency(get_currency(info.quote_currency.to_uppercase().as_str()))
79 .price_precision(price_increment.precision)
80 .size_precision(size_increment.precision)
81 .price_increment(price_increment)
82 .size_increment(size_increment)
83 .maybe_multiplier(multiplier)
84 .lot_size(size_increment)
85 .min_quantity(Quantity::from(info.min_trade_amount.to_string()))
86 .margin_init(margin_init)
87 .margin_maint(margin_maint)
88 .maker_fee(maker_fee)
89 .taker_fee(taker_fee)
90 .maybe_info(build_info_params(info))
91 .ts_event(ts_event)
92 .ts_init(ts_init)
93 .build()
94 .unwrap(),
95 )
96}
97
98#[expect(clippy::too_many_arguments)]
104#[must_use]
105pub fn create_crypto_perpetual(
106 info: &TardisInstrumentInfo,
107 instrument_id: InstrumentId,
108 raw_symbol: Symbol,
109 price_increment: Price,
110 size_increment: Quantity,
111 multiplier: Option<Quantity>,
112 margin_init: Decimal,
113 margin_maint: Decimal,
114 maker_fee: Decimal,
115 taker_fee: Decimal,
116 ts_event: UnixNanos,
117 ts_init: UnixNanos,
118) -> InstrumentAny {
119 let is_inverse = info.inverse.unwrap_or(false);
120
121 InstrumentAny::CryptoPerpetual(
122 CryptoPerpetual::builder()
123 .instrument_id(instrument_id)
124 .raw_symbol(raw_symbol)
125 .base_currency(get_currency(info.base_currency.to_uppercase().as_str()))
126 .quote_currency(get_currency(info.quote_currency.to_uppercase().as_str()))
127 .settlement_currency(get_currency(
128 parse_settlement_currency(info, is_inverse).as_str(),
129 ))
130 .is_inverse(is_inverse)
131 .price_precision(price_increment.precision)
132 .size_precision(size_increment.precision)
133 .price_increment(price_increment)
134 .size_increment(size_increment)
135 .maybe_multiplier(multiplier)
136 .lot_size(size_increment)
137 .min_quantity(Quantity::from(info.min_trade_amount.to_string()))
138 .margin_init(margin_init)
139 .margin_maint(margin_maint)
140 .maker_fee(maker_fee)
141 .taker_fee(taker_fee)
142 .maybe_info(build_info_params(info))
143 .ts_event(ts_event)
144 .ts_init(ts_init)
145 .build()
146 .unwrap(),
147 )
148}
149
150#[expect(clippy::too_many_arguments)]
156#[must_use]
157pub fn create_crypto_future(
158 info: &TardisInstrumentInfo,
159 instrument_id: InstrumentId,
160 raw_symbol: Symbol,
161 activation: UnixNanos,
162 expiration: UnixNanos,
163 price_increment: Price,
164 size_increment: Quantity,
165 multiplier: Option<Quantity>,
166 margin_init: Decimal,
167 margin_maint: Decimal,
168 maker_fee: Decimal,
169 taker_fee: Decimal,
170 ts_event: UnixNanos,
171 ts_init: UnixNanos,
172) -> InstrumentAny {
173 let is_inverse = info.inverse.unwrap_or(false);
174
175 InstrumentAny::CryptoFuture(
176 CryptoFuture::builder()
177 .instrument_id(instrument_id)
178 .raw_symbol(raw_symbol)
179 .underlying(get_currency(info.base_currency.to_uppercase().as_str()))
180 .quote_currency(get_currency(info.quote_currency.to_uppercase().as_str()))
181 .settlement_currency(get_currency(
182 parse_settlement_currency(info, is_inverse).as_str(),
183 ))
184 .is_inverse(is_inverse)
185 .activation_ns(activation)
186 .expiration_ns(expiration)
187 .price_precision(price_increment.precision)
188 .size_precision(size_increment.precision)
189 .price_increment(price_increment)
190 .size_increment(size_increment)
191 .maybe_multiplier(multiplier)
192 .lot_size(size_increment)
193 .min_quantity(Quantity::from(info.min_trade_amount.to_string()))
194 .margin_init(margin_init)
195 .margin_maint(margin_maint)
196 .maker_fee(maker_fee)
197 .taker_fee(taker_fee)
198 .maybe_info(build_info_params(info))
199 .ts_event(ts_event)
200 .ts_init(ts_init)
201 .build()
202 .unwrap(),
203 )
204}
205
206#[expect(clippy::too_many_arguments)]
207pub fn create_crypto_option(
217 info: &TardisInstrumentInfo,
218 instrument_id: InstrumentId,
219 raw_symbol: Symbol,
220 activation: UnixNanos,
221 expiration: UnixNanos,
222 price_increment: Price,
223 size_increment: Quantity,
224 multiplier: Option<Quantity>,
225 margin_init: Decimal,
226 margin_maint: Decimal,
227 maker_fee: Decimal,
228 taker_fee: Decimal,
229 ts_event: UnixNanos,
230 ts_init: UnixNanos,
231) -> anyhow::Result<InstrumentAny> {
232 let is_inverse = info.inverse.unwrap_or(false);
233
234 let option_type = info.option_type.ok_or_else(|| {
235 anyhow::anyhow!(
236 "CryptoOption missing `option_type` field for instrument: {}",
237 info.id
238 )
239 })?;
240
241 let strike_price = info.strike_price.ok_or_else(|| {
242 anyhow::anyhow!(
243 "CryptoOption missing `strike_price` field for instrument: {}",
244 info.id
245 )
246 })?;
247
248 Ok(InstrumentAny::CryptoOption(
249 CryptoOption::builder()
250 .instrument_id(instrument_id)
251 .raw_symbol(raw_symbol)
252 .underlying(get_currency(info.base_currency.to_uppercase().as_str()))
253 .quote_currency(get_currency(info.quote_currency.to_uppercase().as_str()))
254 .settlement_currency(get_currency(
255 parse_settlement_currency(info, is_inverse).as_str(),
256 ))
257 .is_inverse(is_inverse)
258 .option_kind(parse_option_kind(option_type))
259 .strike_price(Price::new(strike_price, price_increment.precision))
260 .activation_ns(activation)
261 .expiration_ns(expiration)
262 .price_precision(price_increment.precision)
263 .size_precision(size_increment.precision)
264 .price_increment(price_increment)
265 .size_increment(size_increment)
266 .maybe_multiplier(multiplier)
267 .lot_size(size_increment)
268 .min_quantity(Quantity::from(info.min_trade_amount.to_string()))
269 .margin_init(margin_init)
270 .margin_maint(margin_maint)
271 .maker_fee(maker_fee)
272 .taker_fee(taker_fee)
273 .maybe_info(build_info_params(info))
274 .ts_event(ts_event)
275 .ts_init(ts_init)
276 .build()
277 .unwrap(),
278 ))
279}
280
281pub fn is_available(
283 info: &TardisInstrumentInfo,
284 start: Option<UnixNanos>,
285 end: Option<UnixNanos>,
286 available_offset: Option<UnixNanos>,
287 effective: Option<UnixNanos>,
288) -> bool {
289 let available_since =
290 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
291 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
292
293 if let Some(effective_date) = effective {
294 if available_since >= effective_date || available_to <= effective_date {
296 return false;
297 }
298
299 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
301 return false;
302 }
303 } else {
304 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
306 return false;
307 }
308 }
309
310 true
311}
312
313#[cfg(test)]
314mod tests {
315 use rstest::rstest;
316
317 use super::*;
318 use crate::common::testing::load_test_json;
319
320 fn create_test_instrument(
322 available_since: u64,
323 available_to: Option<u64>,
324 ) -> TardisInstrumentInfo {
325 let json_data = load_test_json("instrument_spot.json");
326 let mut info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
327 info.available_since = UnixNanos::from(available_since).to_datetime_utc();
328 info.available_to = available_to.map(|a| UnixNanos::from(a).to_datetime_utc());
329 info
330 }
331
332 #[rstest]
333 #[case::no_constraints(None, None, None, None, true)]
334 #[case::within_start_end(Some(100), Some(300), None, None, true)]
335 #[case::before_start(Some(200), Some(300), None, None, true)]
336 #[case::after_end(Some(100), Some(150), None, None, true)]
337 #[case::with_offset_within_range(Some(200), Some(300), Some(50), None, true)]
338 #[case::with_offset_adjusted_within_range(Some(150), Some(300), Some(50), None, true)]
339 #[case::effective_within_availability(None, None, None, Some(150), true)]
340 #[case::effective_before_availability(None, None, None, Some(50), false)]
341 #[case::effective_after_availability(None, None, None, Some(250), false)]
342 #[case::effective_within_start_end(Some(100), Some(200), None, Some(150), true)]
343 #[case::effective_before_start(Some(150), Some(200), None, Some(120), false)]
344 #[case::effective_after_end(Some(100), Some(150), None, Some(180), false)]
345 #[case::effective_equals_available_since(None, None, None, Some(100), false)]
346 #[case::effective_equals_available_to(None, None, None, Some(200), false)]
347 fn test_is_available(
348 #[case] start: Option<u64>,
349 #[case] end: Option<u64>,
350 #[case] available_offset: Option<u64>,
351 #[case] effective: Option<u64>,
352 #[case] expected: bool,
353 ) {
354 let info = create_test_instrument(100, Some(200));
356
357 let start_nanos = start.map(UnixNanos::from);
359 let end_nanos = end.map(UnixNanos::from);
360 let offset_nanos = available_offset.map(UnixNanos::from);
361 let effective_nanos = effective.map(UnixNanos::from);
362
363 let result = is_available(&info, start_nanos, end_nanos, offset_nanos, effective_nanos);
365
366 assert_eq!(
367 result, expected,
368 "Test failed with start={start:?}, end={end:?}, offset={available_offset:?}, effective={effective:?}"
369 );
370 }
371
372 #[rstest]
373 fn test_infinite_available_to() {
374 let info = create_test_instrument(100, None);
376
377 assert!(is_available(
379 &info,
380 None,
381 Some(UnixNanos::from(1000000)),
382 None,
383 None
384 ));
385
386 assert!(is_available(
388 &info,
389 None,
390 None,
391 None,
392 Some(UnixNanos::from(101))
393 ));
394
395 assert!(!is_available(
397 &info,
398 None,
399 None,
400 None,
401 Some(UnixNanos::from(100))
402 ));
403 assert!(!is_available(
404 &info,
405 None,
406 None,
407 None,
408 Some(UnixNanos::from(99))
409 ));
410 }
411
412 #[rstest]
413 fn test_available_offset_effects() {
414 let info = create_test_instrument(100, Some(200));
416
417 assert!(!is_available(
419 &info,
420 None,
421 None,
422 None,
423 Some(UnixNanos::from(100))
424 ));
425
426 assert!(!is_available(
428 &info,
429 None,
430 None,
431 Some(UnixNanos::from(10)),
432 Some(UnixNanos::from(100))
433 ));
434
435 assert!(!is_available(
437 &info,
438 None,
439 None,
440 Some(UnixNanos::from(20)),
441 Some(UnixNanos::from(119))
442 ));
443 assert!(is_available(
444 &info,
445 None,
446 None,
447 Some(UnixNanos::from(20)),
448 Some(UnixNanos::from(121))
449 ));
450 }
451
452 #[rstest]
453 fn test_with_real_dates() {
454 let info = create_test_instrument(1682294400000, Some(1712061000000));
459
460 let mid_date = UnixNanos::from(1695000000000); assert!(is_available(&info, None, None, None, Some(mid_date)));
463
464 let start = UnixNanos::from(1690000000000); let end = UnixNanos::from(1700000000000); assert!(is_available(
468 &info,
469 Some(start),
470 Some(end),
471 None,
472 Some(mid_date)
473 ));
474
475 let offset = UnixNanos::from(86400000); let day_after_start = UnixNanos::from(1682294400000 + 86400000);
480 assert!(!is_available(
481 &info,
482 None,
483 None,
484 Some(offset),
485 Some(day_after_start)
486 ));
487
488 let start_date = UnixNanos::from(1682294400000);
490 assert!(!is_available(&info, None, None, None, Some(start_date)));
491
492 let end_date = UnixNanos::from(1712061000000);
494 assert!(!is_available(&info, None, None, None, Some(end_date)));
495 }
496
497 #[rstest]
498 fn test_complex_scenarios() {
499 let info = create_test_instrument(100, Some(200));
501
502 assert!(is_available(
504 &info,
505 Some(UnixNanos::from(150)),
506 Some(UnixNanos::from(250)),
507 None,
508 None
509 ));
510 assert!(is_available(
511 &info,
512 Some(UnixNanos::from(50)),
513 Some(UnixNanos::from(150)),
514 None,
515 None
516 ));
517
518 assert!(is_available(
520 &info,
521 Some(UnixNanos::from(50)),
522 Some(UnixNanos::from(250)),
523 None,
524 None
525 ));
526
527 assert!(is_available(
529 &info,
530 Some(UnixNanos::from(120)),
531 Some(UnixNanos::from(180)),
532 None,
533 None
534 ));
535
536 assert!(is_available(
538 &info,
539 Some(UnixNanos::from(120)),
540 Some(UnixNanos::from(180)),
541 None,
542 Some(UnixNanos::from(150))
543 ));
544
545 assert!(!is_available(
547 &info,
548 Some(UnixNanos::from(120)),
549 Some(UnixNanos::from(140)),
550 None,
551 Some(UnixNanos::from(150))
552 ));
553 }
554
555 #[rstest]
556 fn test_edge_cases() {
557 let mut info = create_test_instrument(100, Some(200));
559 info.changes = Some(vec![]);
560 assert!(is_available(
561 &info,
562 None,
563 None,
564 None,
565 Some(UnixNanos::from(150))
566 ));
567
568 let far_future_info = create_test_instrument(100, None); let far_future_date = UnixNanos::from(u64::MAX - 1000);
571 assert!(is_available(
572 &far_future_info,
573 None,
574 None,
575 None,
576 Some(UnixNanos::from(101))
577 ));
578 assert!(is_available(
579 &far_future_info,
580 None,
581 Some(far_future_date),
582 None,
583 None
584 ));
585
586 let info = create_test_instrument(100, Some(200));
588
589 let offset = UnixNanos::from(50);
591 assert!(!is_available(
592 &info,
593 None,
594 None,
595 Some(offset),
596 Some(UnixNanos::from(149))
597 ));
598 assert!(is_available(
599 &info,
600 None,
601 None,
602 Some(offset),
603 Some(UnixNanos::from(151))
604 ));
605
606 let zero_offset = UnixNanos::from(0);
608 assert!(!is_available(
609 &info,
610 None,
611 None,
612 Some(zero_offset),
613 Some(UnixNanos::from(100))
614 ));
615 assert!(is_available(
616 &info,
617 None,
618 None,
619 Some(zero_offset),
620 Some(UnixNanos::from(101))
621 ));
622 }
623}