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)]
53#[must_use]
54pub fn create_currency_pair(
55 info: &TardisInstrumentInfo,
56 instrument_id: InstrumentId,
57 raw_symbol: Symbol,
58 price_increment: Price,
59 size_increment: Quantity,
60 multiplier: Option<Quantity>,
61 margin_init: Decimal,
62 margin_maint: Decimal,
63 maker_fee: Decimal,
64 taker_fee: Decimal,
65 ts_event: UnixNanos,
66 ts_init: UnixNanos,
67) -> InstrumentAny {
68 InstrumentAny::CurrencyPair(CurrencyPair::new(
69 instrument_id,
70 raw_symbol,
71 get_currency(info.base_currency.to_uppercase().as_str()),
72 get_currency(info.quote_currency.to_uppercase().as_str()),
73 price_increment.precision,
74 size_increment.precision,
75 price_increment,
76 size_increment,
77 multiplier,
78 Some(size_increment),
79 None,
80 Some(Quantity::from(info.min_trade_amount.to_string())),
81 None,
82 None,
83 None,
84 None,
85 Some(margin_init),
86 Some(margin_maint),
87 Some(maker_fee),
88 Some(taker_fee),
89 None,
90 build_info_params(info),
91 ts_event,
92 ts_init,
93 ))
94}
95
96#[expect(clippy::too_many_arguments)]
97#[must_use]
98pub fn create_crypto_perpetual(
99 info: &TardisInstrumentInfo,
100 instrument_id: InstrumentId,
101 raw_symbol: Symbol,
102 price_increment: Price,
103 size_increment: Quantity,
104 multiplier: Option<Quantity>,
105 margin_init: Decimal,
106 margin_maint: Decimal,
107 maker_fee: Decimal,
108 taker_fee: Decimal,
109 ts_event: UnixNanos,
110 ts_init: UnixNanos,
111) -> InstrumentAny {
112 let is_inverse = info.inverse.unwrap_or(false);
113
114 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
115 instrument_id,
116 raw_symbol,
117 get_currency(info.base_currency.to_uppercase().as_str()),
118 get_currency(info.quote_currency.to_uppercase().as_str()),
119 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
120 is_inverse,
121 price_increment.precision,
122 size_increment.precision,
123 price_increment,
124 size_increment,
125 multiplier,
126 Some(size_increment),
127 None,
128 Some(Quantity::from(info.min_trade_amount.to_string())),
129 None,
130 None,
131 None,
132 None,
133 Some(margin_init),
134 Some(margin_maint),
135 Some(maker_fee),
136 Some(taker_fee),
137 None,
138 build_info_params(info),
139 ts_event,
140 ts_init,
141 ))
142}
143
144#[expect(clippy::too_many_arguments)]
145#[must_use]
146pub fn create_crypto_future(
147 info: &TardisInstrumentInfo,
148 instrument_id: InstrumentId,
149 raw_symbol: Symbol,
150 activation: UnixNanos,
151 expiration: UnixNanos,
152 price_increment: Price,
153 size_increment: Quantity,
154 multiplier: Option<Quantity>,
155 margin_init: Decimal,
156 margin_maint: Decimal,
157 maker_fee: Decimal,
158 taker_fee: Decimal,
159 ts_event: UnixNanos,
160 ts_init: UnixNanos,
161) -> InstrumentAny {
162 let is_inverse = info.inverse.unwrap_or(false);
163
164 InstrumentAny::CryptoFuture(CryptoFuture::new(
165 instrument_id,
166 raw_symbol,
167 get_currency(info.base_currency.to_uppercase().as_str()),
168 get_currency(info.quote_currency.to_uppercase().as_str()),
169 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
170 is_inverse,
171 activation,
172 expiration,
173 price_increment.precision,
174 size_increment.precision,
175 price_increment,
176 size_increment,
177 multiplier,
178 Some(size_increment),
179 None,
180 Some(Quantity::from(info.min_trade_amount.to_string())),
181 None,
182 None,
183 None,
184 None,
185 Some(margin_init),
186 Some(margin_maint),
187 Some(maker_fee),
188 Some(taker_fee),
189 None,
190 build_info_params(info),
191 ts_event,
192 ts_init,
193 ))
194}
195
196#[expect(clippy::too_many_arguments)]
197pub fn create_crypto_option(
203 info: &TardisInstrumentInfo,
204 instrument_id: InstrumentId,
205 raw_symbol: Symbol,
206 activation: UnixNanos,
207 expiration: UnixNanos,
208 price_increment: Price,
209 size_increment: Quantity,
210 multiplier: Option<Quantity>,
211 margin_init: Decimal,
212 margin_maint: Decimal,
213 maker_fee: Decimal,
214 taker_fee: Decimal,
215 ts_event: UnixNanos,
216 ts_init: UnixNanos,
217) -> anyhow::Result<InstrumentAny> {
218 let is_inverse = info.inverse.unwrap_or(false);
219
220 let option_type = info.option_type.ok_or_else(|| {
221 anyhow::anyhow!(
222 "CryptoOption missing `option_type` field for instrument: {}",
223 info.id
224 )
225 })?;
226
227 let strike_price = info.strike_price.ok_or_else(|| {
228 anyhow::anyhow!(
229 "CryptoOption missing `strike_price` field for instrument: {}",
230 info.id
231 )
232 })?;
233
234 Ok(InstrumentAny::CryptoOption(CryptoOption::new(
235 instrument_id,
236 raw_symbol,
237 get_currency(info.base_currency.to_uppercase().as_str()),
238 get_currency(info.quote_currency.to_uppercase().as_str()),
239 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
240 is_inverse,
241 parse_option_kind(option_type),
242 Price::new(strike_price, price_increment.precision),
243 activation,
244 expiration,
245 price_increment.precision,
246 size_increment.precision,
247 price_increment,
248 size_increment,
249 multiplier,
250 Some(size_increment),
251 None,
252 Some(Quantity::from(info.min_trade_amount.to_string())),
253 None,
254 None,
255 None,
256 None,
257 Some(margin_init),
258 Some(margin_maint),
259 Some(maker_fee),
260 Some(taker_fee),
261 None,
262 build_info_params(info),
263 ts_event,
264 ts_init,
265 )))
266}
267
268pub fn is_available(
270 info: &TardisInstrumentInfo,
271 start: Option<UnixNanos>,
272 end: Option<UnixNanos>,
273 available_offset: Option<UnixNanos>,
274 effective: Option<UnixNanos>,
275) -> bool {
276 let available_since =
277 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
278 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
279
280 if let Some(effective_date) = effective {
281 if available_since >= effective_date || available_to <= effective_date {
283 return false;
284 }
285
286 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
288 return false;
289 }
290 } else {
291 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
293 return false;
294 }
295 }
296
297 true
298}
299
300#[cfg(test)]
301mod tests {
302 use rstest::rstest;
303
304 use super::*;
305 use crate::common::testing::load_test_json;
306
307 fn create_test_instrument(
309 available_since: u64,
310 available_to: Option<u64>,
311 ) -> TardisInstrumentInfo {
312 let json_data = load_test_json("instrument_spot.json");
313 let mut info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
314 info.available_since = UnixNanos::from(available_since).to_datetime_utc();
315 info.available_to = available_to.map(|a| UnixNanos::from(a).to_datetime_utc());
316 info
317 }
318
319 #[rstest]
320 #[case::no_constraints(None, None, None, None, true)]
321 #[case::within_start_end(Some(100), Some(300), None, None, true)]
322 #[case::before_start(Some(200), Some(300), None, None, true)]
323 #[case::after_end(Some(100), Some(150), None, None, true)]
324 #[case::with_offset_within_range(Some(200), Some(300), Some(50), None, true)]
325 #[case::with_offset_adjusted_within_range(Some(150), Some(300), Some(50), None, true)]
326 #[case::effective_within_availability(None, None, None, Some(150), true)]
327 #[case::effective_before_availability(None, None, None, Some(50), false)]
328 #[case::effective_after_availability(None, None, None, Some(250), false)]
329 #[case::effective_within_start_end(Some(100), Some(200), None, Some(150), true)]
330 #[case::effective_before_start(Some(150), Some(200), None, Some(120), false)]
331 #[case::effective_after_end(Some(100), Some(150), None, Some(180), false)]
332 #[case::effective_equals_available_since(None, None, None, Some(100), false)]
333 #[case::effective_equals_available_to(None, None, None, Some(200), false)]
334 fn test_is_available(
335 #[case] start: Option<u64>,
336 #[case] end: Option<u64>,
337 #[case] available_offset: Option<u64>,
338 #[case] effective: Option<u64>,
339 #[case] expected: bool,
340 ) {
341 let info = create_test_instrument(100, Some(200));
343
344 let start_nanos = start.map(UnixNanos::from);
346 let end_nanos = end.map(UnixNanos::from);
347 let offset_nanos = available_offset.map(UnixNanos::from);
348 let effective_nanos = effective.map(UnixNanos::from);
349
350 let result = is_available(&info, start_nanos, end_nanos, offset_nanos, effective_nanos);
352
353 assert_eq!(
354 result, expected,
355 "Test failed with start={start:?}, end={end:?}, offset={available_offset:?}, effective={effective:?}"
356 );
357 }
358
359 #[rstest]
360 fn test_infinite_available_to() {
361 let info = create_test_instrument(100, None);
363
364 assert!(is_available(
366 &info,
367 None,
368 Some(UnixNanos::from(1000000)),
369 None,
370 None
371 ));
372
373 assert!(is_available(
375 &info,
376 None,
377 None,
378 None,
379 Some(UnixNanos::from(101))
380 ));
381
382 assert!(!is_available(
384 &info,
385 None,
386 None,
387 None,
388 Some(UnixNanos::from(100))
389 ));
390 assert!(!is_available(
391 &info,
392 None,
393 None,
394 None,
395 Some(UnixNanos::from(99))
396 ));
397 }
398
399 #[rstest]
400 fn test_available_offset_effects() {
401 let info = create_test_instrument(100, Some(200));
403
404 assert!(!is_available(
406 &info,
407 None,
408 None,
409 None,
410 Some(UnixNanos::from(100))
411 ));
412
413 assert!(!is_available(
415 &info,
416 None,
417 None,
418 Some(UnixNanos::from(10)),
419 Some(UnixNanos::from(100))
420 ));
421
422 assert!(!is_available(
424 &info,
425 None,
426 None,
427 Some(UnixNanos::from(20)),
428 Some(UnixNanos::from(119))
429 ));
430 assert!(is_available(
431 &info,
432 None,
433 None,
434 Some(UnixNanos::from(20)),
435 Some(UnixNanos::from(121))
436 ));
437 }
438
439 #[rstest]
440 fn test_with_real_dates() {
441 let info = create_test_instrument(1682294400000, Some(1712061000000));
446
447 let mid_date = UnixNanos::from(1695000000000); assert!(is_available(&info, None, None, None, Some(mid_date)));
450
451 let start = UnixNanos::from(1690000000000); let end = UnixNanos::from(1700000000000); assert!(is_available(
455 &info,
456 Some(start),
457 Some(end),
458 None,
459 Some(mid_date)
460 ));
461
462 let offset = UnixNanos::from(86400000); let day_after_start = UnixNanos::from(1682294400000 + 86400000);
467 assert!(!is_available(
468 &info,
469 None,
470 None,
471 Some(offset),
472 Some(day_after_start)
473 ));
474
475 let start_date = UnixNanos::from(1682294400000);
477 assert!(!is_available(&info, None, None, None, Some(start_date)));
478
479 let end_date = UnixNanos::from(1712061000000);
481 assert!(!is_available(&info, None, None, None, Some(end_date)));
482 }
483
484 #[rstest]
485 fn test_complex_scenarios() {
486 let info = create_test_instrument(100, Some(200));
488
489 assert!(is_available(
491 &info,
492 Some(UnixNanos::from(150)),
493 Some(UnixNanos::from(250)),
494 None,
495 None
496 ));
497 assert!(is_available(
498 &info,
499 Some(UnixNanos::from(50)),
500 Some(UnixNanos::from(150)),
501 None,
502 None
503 ));
504
505 assert!(is_available(
507 &info,
508 Some(UnixNanos::from(50)),
509 Some(UnixNanos::from(250)),
510 None,
511 None
512 ));
513
514 assert!(is_available(
516 &info,
517 Some(UnixNanos::from(120)),
518 Some(UnixNanos::from(180)),
519 None,
520 None
521 ));
522
523 assert!(is_available(
525 &info,
526 Some(UnixNanos::from(120)),
527 Some(UnixNanos::from(180)),
528 None,
529 Some(UnixNanos::from(150))
530 ));
531
532 assert!(!is_available(
534 &info,
535 Some(UnixNanos::from(120)),
536 Some(UnixNanos::from(140)),
537 None,
538 Some(UnixNanos::from(150))
539 ));
540 }
541
542 #[rstest]
543 fn test_edge_cases() {
544 let mut info = create_test_instrument(100, Some(200));
546 info.changes = Some(vec![]);
547 assert!(is_available(
548 &info,
549 None,
550 None,
551 None,
552 Some(UnixNanos::from(150))
553 ));
554
555 let far_future_info = create_test_instrument(100, None); let far_future_date = UnixNanos::from(u64::MAX - 1000);
558 assert!(is_available(
559 &far_future_info,
560 None,
561 None,
562 None,
563 Some(UnixNanos::from(101))
564 ));
565 assert!(is_available(
566 &far_future_info,
567 None,
568 Some(far_future_date),
569 None,
570 None
571 ));
572
573 let info = create_test_instrument(100, Some(200));
575
576 let offset = UnixNanos::from(50);
578 assert!(!is_available(
579 &info,
580 None,
581 None,
582 Some(offset),
583 Some(UnixNanos::from(149))
584 ));
585 assert!(is_available(
586 &info,
587 None,
588 None,
589 Some(offset),
590 Some(UnixNanos::from(151))
591 ));
592
593 let zero_offset = UnixNanos::from(0);
595 assert!(!is_available(
596 &info,
597 None,
598 None,
599 Some(zero_offset),
600 Some(UnixNanos::from(100))
601 ));
602 assert!(is_available(
603 &info,
604 None,
605 None,
606 Some(zero_offset),
607 Some(UnixNanos::from(101))
608 ));
609 }
610}