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