1use nautilus_core::UnixNanos;
17use nautilus_model::{
18 currencies::CURRENCY_MAP,
19 enums::CurrencyType,
20 identifiers::{InstrumentId, Symbol},
21 instruments::{CryptoFuture, CryptoOption, CryptoPerpetual, CurrencyPair, InstrumentAny},
22 types::{Currency, Price, Quantity},
23};
24use rust_decimal::Decimal;
25
26use super::{models::TardisInstrumentInfo, parse::parse_settlement_currency};
27use crate::parse::parse_option_kind;
28
29pub(crate) fn get_currency(code: &str) -> Currency {
31 CURRENCY_MAP
33 .lock()
34 .expect("Failed to acquire CURRENCY_MAP lock")
35 .get(code)
36 .copied()
37 .unwrap_or(Currency::new(code, 8, 0, code, CurrencyType::Crypto))
38}
39
40#[allow(clippy::too_many_arguments)]
41#[must_use]
42pub fn create_currency_pair(
43 info: &TardisInstrumentInfo,
44 instrument_id: InstrumentId,
45 raw_symbol: Symbol,
46 price_increment: Price,
47 size_increment: Quantity,
48 multiplier: Option<Quantity>,
49 margin_init: Decimal,
50 margin_maint: Decimal,
51 maker_fee: Decimal,
52 taker_fee: Decimal,
53 ts_event: UnixNanos,
54 ts_init: UnixNanos,
55) -> InstrumentAny {
56 InstrumentAny::CurrencyPair(CurrencyPair::new(
57 instrument_id,
58 raw_symbol,
59 get_currency(info.base_currency.to_uppercase().as_str()),
60 get_currency(info.quote_currency.to_uppercase().as_str()),
61 price_increment.precision,
62 size_increment.precision,
63 price_increment,
64 size_increment,
65 multiplier,
66 None,
67 None,
68 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
69 None,
70 None,
71 None,
72 None,
73 Some(margin_init),
74 Some(margin_maint),
75 Some(maker_fee),
76 Some(taker_fee),
77 ts_event,
78 ts_init,
79 ))
80}
81
82#[allow(clippy::too_many_arguments)]
83#[must_use]
84pub fn create_crypto_perpetual(
85 info: &TardisInstrumentInfo,
86 instrument_id: InstrumentId,
87 raw_symbol: Symbol,
88 price_increment: Price,
89 size_increment: Quantity,
90 multiplier: Option<Quantity>,
91 margin_init: Decimal,
92 margin_maint: Decimal,
93 maker_fee: Decimal,
94 taker_fee: Decimal,
95 ts_event: UnixNanos,
96 ts_init: UnixNanos,
97) -> InstrumentAny {
98 let is_inverse = info.inverse.unwrap_or(false);
99
100 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
101 instrument_id,
102 raw_symbol,
103 get_currency(info.base_currency.to_uppercase().as_str()),
104 get_currency(info.quote_currency.to_uppercase().as_str()),
105 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
106 is_inverse,
107 price_increment.precision,
108 size_increment.precision,
109 price_increment,
110 size_increment,
111 multiplier,
112 None, None,
114 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
115 None,
116 None,
117 None,
118 None,
119 Some(margin_init),
120 Some(margin_maint),
121 Some(maker_fee),
122 Some(taker_fee),
123 ts_event,
124 ts_init,
125 ))
126}
127
128#[allow(clippy::too_many_arguments)]
129#[must_use]
130pub fn create_crypto_future(
131 info: &TardisInstrumentInfo,
132 instrument_id: InstrumentId,
133 raw_symbol: Symbol,
134 activation: UnixNanos,
135 expiration: UnixNanos,
136 price_increment: Price,
137 size_increment: Quantity,
138 multiplier: Option<Quantity>,
139 margin_init: Decimal,
140 margin_maint: Decimal,
141 maker_fee: Decimal,
142 taker_fee: Decimal,
143 ts_event: UnixNanos,
144 ts_init: UnixNanos,
145) -> InstrumentAny {
146 let is_inverse = info.inverse.unwrap_or(false);
147
148 InstrumentAny::CryptoFuture(CryptoFuture::new(
149 instrument_id,
150 raw_symbol,
151 get_currency(info.base_currency.to_uppercase().as_str()),
152 get_currency(info.quote_currency.to_uppercase().as_str()),
153 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
154 is_inverse,
155 activation,
156 expiration,
157 price_increment.precision,
158 size_increment.precision,
159 price_increment,
160 size_increment,
161 multiplier,
162 None, None,
164 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
165 None,
166 None,
167 None,
168 None,
169 Some(margin_init),
170 Some(margin_maint),
171 Some(maker_fee),
172 Some(taker_fee),
173 ts_event,
174 ts_init,
175 ))
176}
177
178#[allow(clippy::too_many_arguments)]
179#[must_use]
185pub fn create_crypto_option(
186 info: &TardisInstrumentInfo,
187 instrument_id: InstrumentId,
188 raw_symbol: Symbol,
189 activation: UnixNanos,
190 expiration: UnixNanos,
191 price_increment: Price,
192 size_increment: Quantity,
193 multiplier: Option<Quantity>,
194 margin_init: Decimal,
195 margin_maint: Decimal,
196 maker_fee: Decimal,
197 taker_fee: Decimal,
198 ts_event: UnixNanos,
199 ts_init: UnixNanos,
200) -> InstrumentAny {
201 let is_inverse = info.inverse.unwrap_or(false);
202
203 InstrumentAny::CryptoOption(CryptoOption::new(
204 instrument_id,
205 raw_symbol,
206 get_currency(info.base_currency.to_uppercase().as_str()),
207 get_currency(info.quote_currency.to_uppercase().as_str()),
208 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
209 is_inverse,
210 parse_option_kind(
211 info.option_type
212 .expect("CryptoOption should have `option_type` field"),
213 ),
214 Price::new(
215 info.strike_price
216 .expect("CryptoOption should have `strike_price` field"),
217 price_increment.precision,
218 ),
219 activation,
220 expiration,
221 price_increment.precision,
222 size_increment.precision,
223 price_increment,
224 size_increment,
225 multiplier,
226 None,
227 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
228 None,
229 None,
230 None,
231 None,
232 Some(margin_init),
233 Some(margin_maint),
234 Some(maker_fee),
235 Some(taker_fee),
236 ts_event,
237 ts_init,
238 ))
239}
240
241pub fn is_available(
243 info: &TardisInstrumentInfo,
244 start: Option<UnixNanos>,
245 end: Option<UnixNanos>,
246 available_offset: Option<UnixNanos>,
247 effective: Option<UnixNanos>,
248) -> bool {
249 let available_since =
250 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
251 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
252
253 if let Some(effective_date) = effective {
254 if available_since >= effective_date || available_to <= effective_date {
256 return false;
257 }
258
259 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
261 return false;
262 }
263 } else {
264 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
266 return false;
267 }
268 }
269
270 true
271}
272
273#[cfg(test)]
274mod tests {
275 use rstest::rstest;
276
277 use super::*;
278 use crate::tests::load_test_json;
279
280 fn create_test_instrument(
282 available_since: u64,
283 available_to: Option<u64>,
284 ) -> TardisInstrumentInfo {
285 let json_data = load_test_json("instrument_spot.json");
286 let mut info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
287 info.available_since = UnixNanos::from(available_since).to_datetime_utc();
288 info.available_to = available_to.map(|a| UnixNanos::from(a).to_datetime_utc());
289 info
290 }
291
292 #[rstest]
293 #[case::no_constraints(None, None, None, None, true)]
294 #[case::within_start_end(Some(100), Some(300), None, None, true)]
295 #[case::before_start(Some(200), Some(300), None, None, true)]
296 #[case::after_end(Some(100), Some(150), None, None, true)]
297 #[case::with_offset_within_range(Some(200), Some(300), Some(50), None, true)]
298 #[case::with_offset_adjusted_within_range(Some(150), Some(300), Some(50), None, true)]
299 #[case::effective_within_availability(None, None, None, Some(150), true)]
300 #[case::effective_before_availability(None, None, None, Some(50), false)]
301 #[case::effective_after_availability(None, None, None, Some(250), false)]
302 #[case::effective_within_start_end(Some(100), Some(200), None, Some(150), true)]
303 #[case::effective_before_start(Some(150), Some(200), None, Some(120), false)]
304 #[case::effective_after_end(Some(100), Some(150), None, Some(180), false)]
305 #[case::effective_equals_available_since(None, None, None, Some(100), false)]
306 #[case::effective_equals_available_to(None, None, None, Some(200), false)]
307 fn test_is_available(
308 #[case] start: Option<u64>,
309 #[case] end: Option<u64>,
310 #[case] available_offset: Option<u64>,
311 #[case] effective: Option<u64>,
312 #[case] expected: bool,
313 ) {
314 let info = create_test_instrument(100, Some(200));
316
317 let start_nanos = start.map(UnixNanos::from);
319 let end_nanos = end.map(UnixNanos::from);
320 let offset_nanos = available_offset.map(UnixNanos::from);
321 let effective_nanos = effective.map(UnixNanos::from);
322
323 let result = is_available(&info, start_nanos, end_nanos, offset_nanos, effective_nanos);
325
326 assert_eq!(
327 result, expected,
328 "Test failed with start={start:?}, end={end:?}, offset={available_offset:?}, effective={effective:?}"
329 );
330 }
331
332 #[rstest]
333 fn test_infinite_available_to() {
334 let info = create_test_instrument(100, None);
336
337 assert!(is_available(
339 &info,
340 None,
341 Some(UnixNanos::from(1000000)),
342 None,
343 None
344 ));
345
346 assert!(is_available(
348 &info,
349 None,
350 None,
351 None,
352 Some(UnixNanos::from(101))
353 ));
354
355 assert!(!is_available(
357 &info,
358 None,
359 None,
360 None,
361 Some(UnixNanos::from(100))
362 ));
363 assert!(!is_available(
364 &info,
365 None,
366 None,
367 None,
368 Some(UnixNanos::from(99))
369 ));
370 }
371
372 #[rstest]
373 fn test_available_offset_effects() {
374 let info = create_test_instrument(100, Some(200));
376
377 assert!(!is_available(
379 &info,
380 None,
381 None,
382 None,
383 Some(UnixNanos::from(100))
384 ));
385
386 assert!(!is_available(
388 &info,
389 None,
390 None,
391 Some(UnixNanos::from(10)),
392 Some(UnixNanos::from(100))
393 ));
394
395 assert!(!is_available(
397 &info,
398 None,
399 None,
400 Some(UnixNanos::from(20)),
401 Some(UnixNanos::from(119))
402 ));
403 assert!(is_available(
404 &info,
405 None,
406 None,
407 Some(UnixNanos::from(20)),
408 Some(UnixNanos::from(121))
409 ));
410 }
411
412 #[rstest]
413 fn test_with_real_dates() {
414 let info = create_test_instrument(1682294400000, Some(1712061000000));
419
420 let mid_date = UnixNanos::from(1695000000000); assert!(is_available(&info, None, None, None, Some(mid_date)));
423
424 let start = UnixNanos::from(1690000000000); let end = UnixNanos::from(1700000000000); assert!(is_available(
428 &info,
429 Some(start),
430 Some(end),
431 None,
432 Some(mid_date)
433 ));
434
435 let offset = UnixNanos::from(86400000); let day_after_start = UnixNanos::from(1682294400000 + 86400000);
440 assert!(!is_available(
441 &info,
442 None,
443 None,
444 Some(offset),
445 Some(day_after_start)
446 ));
447
448 let start_date = UnixNanos::from(1682294400000);
450 assert!(!is_available(&info, None, None, None, Some(start_date)));
451
452 let end_date = UnixNanos::from(1712061000000);
454 assert!(!is_available(&info, None, None, None, Some(end_date)));
455 }
456
457 #[rstest]
458 fn test_complex_scenarios() {
459 let info = create_test_instrument(100, Some(200));
461
462 assert!(is_available(
464 &info,
465 Some(UnixNanos::from(150)),
466 Some(UnixNanos::from(250)),
467 None,
468 None
469 ));
470 assert!(is_available(
471 &info,
472 Some(UnixNanos::from(50)),
473 Some(UnixNanos::from(150)),
474 None,
475 None
476 ));
477
478 assert!(is_available(
480 &info,
481 Some(UnixNanos::from(50)),
482 Some(UnixNanos::from(250)),
483 None,
484 None
485 ));
486
487 assert!(is_available(
489 &info,
490 Some(UnixNanos::from(120)),
491 Some(UnixNanos::from(180)),
492 None,
493 None
494 ));
495
496 assert!(is_available(
498 &info,
499 Some(UnixNanos::from(120)),
500 Some(UnixNanos::from(180)),
501 None,
502 Some(UnixNanos::from(150))
503 ));
504
505 assert!(!is_available(
507 &info,
508 Some(UnixNanos::from(120)),
509 Some(UnixNanos::from(140)),
510 None,
511 Some(UnixNanos::from(150))
512 ));
513 }
514
515 #[rstest]
516 fn test_edge_cases() {
517 let mut info = create_test_instrument(100, Some(200));
519 info.changes = Some(vec![]);
520 assert!(is_available(
521 &info,
522 None,
523 None,
524 None,
525 Some(UnixNanos::from(150))
526 ));
527
528 let far_future_info = create_test_instrument(100, None); let far_future_date = UnixNanos::from(u64::MAX - 1000);
531 assert!(is_available(
532 &far_future_info,
533 None,
534 None,
535 None,
536 Some(UnixNanos::from(101))
537 ));
538 assert!(is_available(
539 &far_future_info,
540 None,
541 Some(far_future_date),
542 None,
543 None
544 ));
545
546 let info = create_test_instrument(100, Some(200));
548
549 let offset = UnixNanos::from(50);
551 assert!(!is_available(
552 &info,
553 None,
554 None,
555 Some(offset),
556 Some(UnixNanos::from(149))
557 ));
558 assert!(is_available(
559 &info,
560 None,
561 None,
562 Some(offset),
563 Some(UnixNanos::from(151))
564 ));
565
566 let zero_offset = UnixNanos::from(0);
568 assert!(!is_available(
569 &info,
570 None,
571 None,
572 Some(zero_offset),
573 Some(UnixNanos::from(100))
574 ));
575 assert!(is_available(
576 &info,
577 None,
578 None,
579 Some(zero_offset),
580 Some(UnixNanos::from(101))
581 ));
582 }
583}