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::InstrumentInfo, 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: &InstrumentInfo,
44 instrument_id: InstrumentId,
45 raw_symbol: Symbol,
46 price_increment: Price,
47 size_increment: Quantity,
48 margin_init: Decimal,
49 margin_maint: Decimal,
50 maker_fee: Decimal,
51 taker_fee: Decimal,
52 ts_event: UnixNanos,
53 ts_init: UnixNanos,
54) -> InstrumentAny {
55 InstrumentAny::CurrencyPair(CurrencyPair::new(
56 instrument_id,
57 raw_symbol,
58 get_currency(info.base_currency.to_uppercase().as_str()),
59 get_currency(info.quote_currency.to_uppercase().as_str()),
60 price_increment.precision,
61 size_increment.precision,
62 price_increment,
63 size_increment,
64 None, None,
66 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
67 None,
68 None,
69 None,
70 None,
71 Some(margin_init),
72 Some(margin_maint),
73 Some(maker_fee),
74 Some(taker_fee),
75 ts_event,
76 ts_init,
77 ))
78}
79
80#[allow(clippy::too_many_arguments)]
81#[must_use]
82pub fn create_crypto_perpetual(
83 info: &InstrumentInfo,
84 instrument_id: InstrumentId,
85 raw_symbol: Symbol,
86 price_increment: Price,
87 size_increment: Quantity,
88 multiplier: Option<Quantity>,
89 margin_init: Decimal,
90 margin_maint: Decimal,
91 maker_fee: Decimal,
92 taker_fee: Decimal,
93 ts_event: UnixNanos,
94 ts_init: UnixNanos,
95) -> InstrumentAny {
96 let is_inverse = info.inverse.unwrap_or(false);
97
98 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
99 instrument_id,
100 raw_symbol,
101 get_currency(info.base_currency.to_uppercase().as_str()),
102 get_currency(info.quote_currency.to_uppercase().as_str()),
103 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
104 is_inverse,
105 price_increment.precision,
106 size_increment.precision,
107 price_increment,
108 size_increment,
109 multiplier,
110 None, None,
112 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
113 None,
114 None,
115 None,
116 None,
117 Some(margin_init),
118 Some(margin_maint),
119 Some(maker_fee),
120 Some(taker_fee),
121 ts_event,
122 ts_init,
123 ))
124}
125
126#[allow(clippy::too_many_arguments)]
127#[must_use]
128pub fn create_crypto_future(
129 info: &InstrumentInfo,
130 instrument_id: InstrumentId,
131 raw_symbol: Symbol,
132 activation: UnixNanos,
133 expiration: UnixNanos,
134 price_increment: Price,
135 size_increment: Quantity,
136 multiplier: Option<Quantity>,
137 margin_init: Decimal,
138 margin_maint: Decimal,
139 maker_fee: Decimal,
140 taker_fee: Decimal,
141 ts_event: UnixNanos,
142 ts_init: UnixNanos,
143) -> InstrumentAny {
144 let is_inverse = info.inverse.unwrap_or(false);
145
146 InstrumentAny::CryptoFuture(CryptoFuture::new(
147 instrument_id,
148 raw_symbol,
149 get_currency(info.base_currency.to_uppercase().as_str()),
150 get_currency(info.quote_currency.to_uppercase().as_str()),
151 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
152 is_inverse,
153 activation,
154 expiration,
155 price_increment.precision,
156 size_increment.precision,
157 price_increment,
158 size_increment,
159 multiplier,
160 None, None,
162 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
163 None,
164 None,
165 None,
166 None,
167 Some(margin_init),
168 Some(margin_maint),
169 Some(maker_fee),
170 Some(taker_fee),
171 ts_event,
172 ts_init,
173 ))
174}
175
176#[allow(clippy::too_many_arguments)]
177#[must_use]
183pub fn create_crypto_option(
184 info: &InstrumentInfo,
185 instrument_id: InstrumentId,
186 raw_symbol: Symbol,
187 activation: UnixNanos,
188 expiration: UnixNanos,
189 price_increment: Price,
190 size_increment: Quantity,
191 multiplier: Option<Quantity>,
192 margin_init: Decimal,
193 margin_maint: Decimal,
194 maker_fee: Decimal,
195 taker_fee: Decimal,
196 ts_event: UnixNanos,
197 ts_init: UnixNanos,
198) -> InstrumentAny {
199 let is_inverse = info.inverse.unwrap_or(false);
200
201 InstrumentAny::CryptoOption(CryptoOption::new(
202 instrument_id,
203 raw_symbol,
204 get_currency(info.base_currency.to_uppercase().as_str()),
205 get_currency(info.quote_currency.to_uppercase().as_str()),
206 get_currency(parse_settlement_currency(info, is_inverse).as_str()),
207 is_inverse,
208 parse_option_kind(
209 info.option_type
210 .clone()
211 .expect("CryptoOption should have `option_type` field"),
212 ),
213 Price::new(
214 info.strike_price
215 .expect("CryptoOption should have `strike_price` field"),
216 price_increment.precision,
217 ),
218 activation,
219 expiration,
220 price_increment.precision,
221 size_increment.precision,
222 price_increment,
223 size_increment,
224 multiplier,
225 None,
226 Some(Quantity::from(info.min_trade_amount.to_string().as_str())),
227 None,
228 None,
229 None,
230 None,
231 Some(margin_init),
232 Some(margin_maint),
233 Some(maker_fee),
234 Some(taker_fee),
235 ts_event,
236 ts_init,
237 ))
238}
239
240pub fn is_available(
242 info: &InstrumentInfo,
243 start: Option<UnixNanos>,
244 end: Option<UnixNanos>,
245 available_offset: Option<UnixNanos>,
246 effective: Option<UnixNanos>,
247) -> bool {
248 let available_since =
249 UnixNanos::from(info.available_since) + available_offset.unwrap_or_default();
250 let available_to = info.available_to.map_or(UnixNanos::max(), UnixNanos::from);
251
252 if let Some(effective_date) = effective {
253 if available_since >= effective_date || available_to <= effective_date {
255 return false;
256 }
257
258 if start.is_some_and(|s| effective_date < s) || end.is_some_and(|e| effective_date > e) {
260 return false;
261 }
262 } else {
263 if start.is_some_and(|s| available_to < s) || end.is_some_and(|e| available_since > e) {
265 return false;
266 }
267 }
268
269 true
270}
271
272#[cfg(test)]
273mod tests {
274 use rstest::rstest;
275
276 use super::*;
277 use crate::tests::load_test_json;
278
279 fn create_test_instrument(available_since: u64, available_to: Option<u64>) -> InstrumentInfo {
281 let json_data = load_test_json("instrument_spot.json");
282 let mut info: InstrumentInfo = 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 #[test]
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 #[test]
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 #[test]
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 #[test]
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 #[test]
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}