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())),
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())),
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())),
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)]
174pub fn create_crypto_option(
180 info: &TardisInstrumentInfo,
181 instrument_id: InstrumentId,
182 raw_symbol: Symbol,
183 activation: UnixNanos,
184 expiration: UnixNanos,
185 price_increment: Price,
186 size_increment: Quantity,
187 multiplier: Option<Quantity>,
188 margin_init: Decimal,
189 margin_maint: Decimal,
190 maker_fee: Decimal,
191 taker_fee: Decimal,
192 ts_event: UnixNanos,
193 ts_init: UnixNanos,
194) -> anyhow::Result<InstrumentAny> {
195 let is_inverse = info.inverse.unwrap_or(false);
196
197 let option_type = info.option_type.ok_or_else(|| {
198 anyhow::anyhow!(
199 "CryptoOption missing `option_type` field for instrument: {}",
200 info.id
201 )
202 })?;
203
204 let strike_price = info.strike_price.ok_or_else(|| {
205 anyhow::anyhow!(
206 "CryptoOption missing `strike_price` field for instrument: {}",
207 info.id
208 )
209 })?;
210
211 Ok(InstrumentAny::CryptoOption(CryptoOption::new(
212 instrument_id,
213 raw_symbol,
214 get_currency(info.base_currency.to_uppercase().as_str()),
215 get_currency(info.quote_currency.to_uppercase().as_str()),
216 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
217 is_inverse,
218 parse_option_kind(option_type),
219 Price::new(strike_price, price_increment.precision),
220 activation,
221 expiration,
222 price_increment.precision,
223 size_increment.precision,
224 price_increment,
225 size_increment,
226 multiplier,
227 Some(size_increment),
228 None,
229 Some(Quantity::from(info.min_trade_amount.to_string())),
230 None,
231 None,
232 None,
233 None,
234 Some(margin_init),
235 Some(margin_maint),
236 Some(maker_fee),
237 Some(taker_fee),
238 ts_event,
239 ts_init,
240 )))
241}
242
243pub fn is_available(
245 info: &TardisInstrumentInfo,
246 start: Option<UnixNanos>,
247 end: Option<UnixNanos>,
248 available_offset: Option<UnixNanos>,
249 effective: Option<UnixNanos>,
250) -> bool {
251 let available_since =
252 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
253 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
254
255 if let Some(effective_date) = effective {
256 if available_since >= effective_date || available_to <= effective_date {
258 return false;
259 }
260
261 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
263 return false;
264 }
265 } else {
266 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
268 return false;
269 }
270 }
271
272 true
273}
274
275#[cfg(test)]
276mod tests {
277 use rstest::rstest;
278
279 use super::*;
280 use crate::tests::load_test_json;
281
282 fn create_test_instrument(
284 available_since: u64,
285 available_to: Option<u64>,
286 ) -> TardisInstrumentInfo {
287 let json_data = load_test_json("instrument_spot.json");
288 let mut info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
289 info.available_since = UnixNanos::from(available_since).to_datetime_utc();
290 info.available_to = available_to.map(|a| UnixNanos::from(a).to_datetime_utc());
291 info
292 }
293
294 #[rstest]
295 #[case::no_constraints(None, None, None, None, true)]
296 #[case::within_start_end(Some(100), Some(300), None, None, true)]
297 #[case::before_start(Some(200), Some(300), None, None, true)]
298 #[case::after_end(Some(100), Some(150), None, None, true)]
299 #[case::with_offset_within_range(Some(200), Some(300), Some(50), None, true)]
300 #[case::with_offset_adjusted_within_range(Some(150), Some(300), Some(50), None, true)]
301 #[case::effective_within_availability(None, None, None, Some(150), true)]
302 #[case::effective_before_availability(None, None, None, Some(50), false)]
303 #[case::effective_after_availability(None, None, None, Some(250), false)]
304 #[case::effective_within_start_end(Some(100), Some(200), None, Some(150), true)]
305 #[case::effective_before_start(Some(150), Some(200), None, Some(120), false)]
306 #[case::effective_after_end(Some(100), Some(150), None, Some(180), false)]
307 #[case::effective_equals_available_since(None, None, None, Some(100), false)]
308 #[case::effective_equals_available_to(None, None, None, Some(200), false)]
309 fn test_is_available(
310 #[case] start: Option<u64>,
311 #[case] end: Option<u64>,
312 #[case] available_offset: Option<u64>,
313 #[case] effective: Option<u64>,
314 #[case] expected: bool,
315 ) {
316 let info = create_test_instrument(100, Some(200));
318
319 let start_nanos = start.map(UnixNanos::from);
321 let end_nanos = end.map(UnixNanos::from);
322 let offset_nanos = available_offset.map(UnixNanos::from);
323 let effective_nanos = effective.map(UnixNanos::from);
324
325 let result = is_available(&info, start_nanos, end_nanos, offset_nanos, effective_nanos);
327
328 assert_eq!(
329 result, expected,
330 "Test failed with start={start:?}, end={end:?}, offset={available_offset:?}, effective={effective:?}"
331 );
332 }
333
334 #[rstest]
335 fn test_infinite_available_to() {
336 let info = create_test_instrument(100, None);
338
339 assert!(is_available(
341 &info,
342 None,
343 Some(UnixNanos::from(1000000)),
344 None,
345 None
346 ));
347
348 assert!(is_available(
350 &info,
351 None,
352 None,
353 None,
354 Some(UnixNanos::from(101))
355 ));
356
357 assert!(!is_available(
359 &info,
360 None,
361 None,
362 None,
363 Some(UnixNanos::from(100))
364 ));
365 assert!(!is_available(
366 &info,
367 None,
368 None,
369 None,
370 Some(UnixNanos::from(99))
371 ));
372 }
373
374 #[rstest]
375 fn test_available_offset_effects() {
376 let info = create_test_instrument(100, Some(200));
378
379 assert!(!is_available(
381 &info,
382 None,
383 None,
384 None,
385 Some(UnixNanos::from(100))
386 ));
387
388 assert!(!is_available(
390 &info,
391 None,
392 None,
393 Some(UnixNanos::from(10)),
394 Some(UnixNanos::from(100))
395 ));
396
397 assert!(!is_available(
399 &info,
400 None,
401 None,
402 Some(UnixNanos::from(20)),
403 Some(UnixNanos::from(119))
404 ));
405 assert!(is_available(
406 &info,
407 None,
408 None,
409 Some(UnixNanos::from(20)),
410 Some(UnixNanos::from(121))
411 ));
412 }
413
414 #[rstest]
415 fn test_with_real_dates() {
416 let info = create_test_instrument(1682294400000, Some(1712061000000));
421
422 let mid_date = UnixNanos::from(1695000000000); assert!(is_available(&info, None, None, None, Some(mid_date)));
425
426 let start = UnixNanos::from(1690000000000); let end = UnixNanos::from(1700000000000); assert!(is_available(
430 &info,
431 Some(start),
432 Some(end),
433 None,
434 Some(mid_date)
435 ));
436
437 let offset = UnixNanos::from(86400000); let day_after_start = UnixNanos::from(1682294400000 + 86400000);
442 assert!(!is_available(
443 &info,
444 None,
445 None,
446 Some(offset),
447 Some(day_after_start)
448 ));
449
450 let start_date = UnixNanos::from(1682294400000);
452 assert!(!is_available(&info, None, None, None, Some(start_date)));
453
454 let end_date = UnixNanos::from(1712061000000);
456 assert!(!is_available(&info, None, None, None, Some(end_date)));
457 }
458
459 #[rstest]
460 fn test_complex_scenarios() {
461 let info = create_test_instrument(100, Some(200));
463
464 assert!(is_available(
466 &info,
467 Some(UnixNanos::from(150)),
468 Some(UnixNanos::from(250)),
469 None,
470 None
471 ));
472 assert!(is_available(
473 &info,
474 Some(UnixNanos::from(50)),
475 Some(UnixNanos::from(150)),
476 None,
477 None
478 ));
479
480 assert!(is_available(
482 &info,
483 Some(UnixNanos::from(50)),
484 Some(UnixNanos::from(250)),
485 None,
486 None
487 ));
488
489 assert!(is_available(
491 &info,
492 Some(UnixNanos::from(120)),
493 Some(UnixNanos::from(180)),
494 None,
495 None
496 ));
497
498 assert!(is_available(
500 &info,
501 Some(UnixNanos::from(120)),
502 Some(UnixNanos::from(180)),
503 None,
504 Some(UnixNanos::from(150))
505 ));
506
507 assert!(!is_available(
509 &info,
510 Some(UnixNanos::from(120)),
511 Some(UnixNanos::from(140)),
512 None,
513 Some(UnixNanos::from(150))
514 ));
515 }
516
517 #[rstest]
518 fn test_edge_cases() {
519 let mut info = create_test_instrument(100, Some(200));
521 info.changes = Some(vec![]);
522 assert!(is_available(
523 &info,
524 None,
525 None,
526 None,
527 Some(UnixNanos::from(150))
528 ));
529
530 let far_future_info = create_test_instrument(100, None); let far_future_date = UnixNanos::from(u64::MAX - 1000);
533 assert!(is_available(
534 &far_future_info,
535 None,
536 None,
537 None,
538 Some(UnixNanos::from(101))
539 ));
540 assert!(is_available(
541 &far_future_info,
542 None,
543 Some(far_future_date),
544 None,
545 None
546 ));
547
548 let info = create_test_instrument(100, Some(200));
550
551 let offset = UnixNanos::from(50);
553 assert!(!is_available(
554 &info,
555 None,
556 None,
557 Some(offset),
558 Some(UnixNanos::from(149))
559 ));
560 assert!(is_available(
561 &info,
562 None,
563 None,
564 Some(offset),
565 Some(UnixNanos::from(151))
566 ));
567
568 let zero_offset = UnixNanos::from(0);
570 assert!(!is_available(
571 &info,
572 None,
573 None,
574 Some(zero_offset),
575 Some(UnixNanos::from(100))
576 ));
577 assert!(is_available(
578 &info,
579 None,
580 None,
581 Some(zero_offset),
582 Some(UnixNanos::from(101))
583 ));
584 }
585}