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 Some(size_increment),
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 Some(size_increment),
113 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 Some(size_increment),
163 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 Some(size_increment),
227 None,
228 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
229 None,
230 None,
231 None,
232 None,
233 Some(margin_init),
234 Some(margin_maint),
235 Some(maker_fee),
236 Some(taker_fee),
237 ts_event,
238 ts_init,
239 ))
240}
241
242pub fn is_available(
244 info: &TardisInstrumentInfo,
245 start: Option<UnixNanos>,
246 end: Option<UnixNanos>,
247 available_offset: Option<UnixNanos>,
248 effective: Option<UnixNanos>,
249) -> bool {
250 let available_since =
251 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
252 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
253
254 if let Some(effective_date) = effective {
255 if available_since >= effective_date || available_to <= effective_date {
257 return false;
258 }
259
260 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
262 return false;
263 }
264 } else {
265 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
267 return false;
268 }
269 }
270
271 true
272}
273
274#[cfg(test)]
275mod tests {
276 use rstest::rstest;
277
278 use super::*;
279 use crate::tests::load_test_json;
280
281 fn create_test_instrument(
283 available_since: u64,
284 available_to: Option<u64>,
285 ) -> TardisInstrumentInfo {
286 let json_data = load_test_json("instrument_spot.json");
287 let mut info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
288 info.available_since = UnixNanos::from(available_since).to_datetime_utc();
289 info.available_to = available_to.map(|a| UnixNanos::from(a).to_datetime_utc());
290 info
291 }
292
293 #[rstest]
294 #[case::no_constraints(None, None, None, None, true)]
295 #[case::within_start_end(Some(100), Some(300), None, None, true)]
296 #[case::before_start(Some(200), Some(300), None, None, true)]
297 #[case::after_end(Some(100), Some(150), None, None, true)]
298 #[case::with_offset_within_range(Some(200), Some(300), Some(50), None, true)]
299 #[case::with_offset_adjusted_within_range(Some(150), Some(300), Some(50), None, true)]
300 #[case::effective_within_availability(None, None, None, Some(150), true)]
301 #[case::effective_before_availability(None, None, None, Some(50), false)]
302 #[case::effective_after_availability(None, None, None, Some(250), false)]
303 #[case::effective_within_start_end(Some(100), Some(200), None, Some(150), true)]
304 #[case::effective_before_start(Some(150), Some(200), None, Some(120), false)]
305 #[case::effective_after_end(Some(100), Some(150), None, Some(180), false)]
306 #[case::effective_equals_available_since(None, None, None, Some(100), false)]
307 #[case::effective_equals_available_to(None, None, None, Some(200), false)]
308 fn test_is_available(
309 #[case] start: Option<u64>,
310 #[case] end: Option<u64>,
311 #[case] available_offset: Option<u64>,
312 #[case] effective: Option<u64>,
313 #[case] expected: bool,
314 ) {
315 let info = create_test_instrument(100, Some(200));
317
318 let start_nanos = start.map(UnixNanos::from);
320 let end_nanos = end.map(UnixNanos::from);
321 let offset_nanos = available_offset.map(UnixNanos::from);
322 let effective_nanos = effective.map(UnixNanos::from);
323
324 let result = is_available(&info, start_nanos, end_nanos, offset_nanos, effective_nanos);
326
327 assert_eq!(
328 result, expected,
329 "Test failed with start={start:?}, end={end:?}, offset={available_offset:?}, effective={effective:?}"
330 );
331 }
332
333 #[rstest]
334 fn test_infinite_available_to() {
335 let info = create_test_instrument(100, None);
337
338 assert!(is_available(
340 &info,
341 None,
342 Some(UnixNanos::from(1000000)),
343 None,
344 None
345 ));
346
347 assert!(is_available(
349 &info,
350 None,
351 None,
352 None,
353 Some(UnixNanos::from(101))
354 ));
355
356 assert!(!is_available(
358 &info,
359 None,
360 None,
361 None,
362 Some(UnixNanos::from(100))
363 ));
364 assert!(!is_available(
365 &info,
366 None,
367 None,
368 None,
369 Some(UnixNanos::from(99))
370 ));
371 }
372
373 #[rstest]
374 fn test_available_offset_effects() {
375 let info = create_test_instrument(100, Some(200));
377
378 assert!(!is_available(
380 &info,
381 None,
382 None,
383 None,
384 Some(UnixNanos::from(100))
385 ));
386
387 assert!(!is_available(
389 &info,
390 None,
391 None,
392 Some(UnixNanos::from(10)),
393 Some(UnixNanos::from(100))
394 ));
395
396 assert!(!is_available(
398 &info,
399 None,
400 None,
401 Some(UnixNanos::from(20)),
402 Some(UnixNanos::from(119))
403 ));
404 assert!(is_available(
405 &info,
406 None,
407 None,
408 Some(UnixNanos::from(20)),
409 Some(UnixNanos::from(121))
410 ));
411 }
412
413 #[rstest]
414 fn test_with_real_dates() {
415 let info = create_test_instrument(1682294400000, Some(1712061000000));
420
421 let mid_date = UnixNanos::from(1695000000000); assert!(is_available(&info, None, None, None, Some(mid_date)));
424
425 let start = UnixNanos::from(1690000000000); let end = UnixNanos::from(1700000000000); assert!(is_available(
429 &info,
430 Some(start),
431 Some(end),
432 None,
433 Some(mid_date)
434 ));
435
436 let offset = UnixNanos::from(86400000); let day_after_start = UnixNanos::from(1682294400000 + 86400000);
441 assert!(!is_available(
442 &info,
443 None,
444 None,
445 Some(offset),
446 Some(day_after_start)
447 ));
448
449 let start_date = UnixNanos::from(1682294400000);
451 assert!(!is_available(&info, None, None, None, Some(start_date)));
452
453 let end_date = UnixNanos::from(1712061000000);
455 assert!(!is_available(&info, None, None, None, Some(end_date)));
456 }
457
458 #[rstest]
459 fn test_complex_scenarios() {
460 let info = create_test_instrument(100, Some(200));
462
463 assert!(is_available(
465 &info,
466 Some(UnixNanos::from(150)),
467 Some(UnixNanos::from(250)),
468 None,
469 None
470 ));
471 assert!(is_available(
472 &info,
473 Some(UnixNanos::from(50)),
474 Some(UnixNanos::from(150)),
475 None,
476 None
477 ));
478
479 assert!(is_available(
481 &info,
482 Some(UnixNanos::from(50)),
483 Some(UnixNanos::from(250)),
484 None,
485 None
486 ));
487
488 assert!(is_available(
490 &info,
491 Some(UnixNanos::from(120)),
492 Some(UnixNanos::from(180)),
493 None,
494 None
495 ));
496
497 assert!(is_available(
499 &info,
500 Some(UnixNanos::from(120)),
501 Some(UnixNanos::from(180)),
502 None,
503 Some(UnixNanos::from(150))
504 ));
505
506 assert!(!is_available(
508 &info,
509 Some(UnixNanos::from(120)),
510 Some(UnixNanos::from(140)),
511 None,
512 Some(UnixNanos::from(150))
513 ));
514 }
515
516 #[rstest]
517 fn test_edge_cases() {
518 let mut info = create_test_instrument(100, Some(200));
520 info.changes = Some(vec![]);
521 assert!(is_available(
522 &info,
523 None,
524 None,
525 None,
526 Some(UnixNanos::from(150))
527 ));
528
529 let far_future_info = create_test_instrument(100, None); let far_future_date = UnixNanos::from(u64::MAX - 1000);
532 assert!(is_available(
533 &far_future_info,
534 None,
535 None,
536 None,
537 Some(UnixNanos::from(101))
538 ));
539 assert!(is_available(
540 &far_future_info,
541 None,
542 Some(far_future_date),
543 None,
544 None
545 ));
546
547 let info = create_test_instrument(100, Some(200));
549
550 let offset = UnixNanos::from(50);
552 assert!(!is_available(
553 &info,
554 None,
555 None,
556 Some(offset),
557 Some(UnixNanos::from(149))
558 ));
559 assert!(is_available(
560 &info,
561 None,
562 None,
563 Some(offset),
564 Some(UnixNanos::from(151))
565 ));
566
567 let zero_offset = UnixNanos::from(0);
569 assert!(!is_available(
570 &info,
571 None,
572 None,
573 Some(zero_offset),
574 Some(UnixNanos::from(100))
575 ));
576 assert!(is_available(
577 &info,
578 None,
579 None,
580 Some(zero_offset),
581 Some(UnixNanos::from(101))
582 ));
583 }
584}