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