1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{CorrectnessResult, CorrectnessResultExt, FAILED, check_equal_u8},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny};
27use crate::{
28 enums::{AssetClass, InstrumentClass, OptionKind},
29 identifiers::{InstrumentId, Symbol},
30 types::{
31 currency::Currency,
32 money::Money,
33 price::{Price, check_positive_price},
34 quantity::{Quantity, check_positive_quantity},
35 },
36};
37
38#[repr(C)]
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42 feature = "python",
43 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
44)]
45#[cfg_attr(
46 feature = "python",
47 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
48)]
49pub struct CryptoFuture {
50 pub id: InstrumentId,
52 pub raw_symbol: Symbol,
54 pub underlying: Currency,
56 pub quote_currency: Currency,
58 pub settlement_currency: Currency,
60 pub is_inverse: bool,
62 pub activation_ns: UnixNanos,
64 pub expiration_ns: UnixNanos,
66 pub price_precision: u8,
68 pub size_precision: u8,
70 pub price_increment: Price,
72 pub size_increment: Quantity,
74 pub multiplier: Quantity,
76 pub lot_size: Quantity,
78 pub margin_init: Decimal,
80 pub margin_maint: Decimal,
82 pub maker_fee: Decimal,
84 pub taker_fee: Decimal,
86 pub max_quantity: Option<Quantity>,
88 pub min_quantity: Option<Quantity>,
90 pub max_notional: Option<Money>,
92 pub min_notional: Option<Money>,
94 pub max_price: Option<Price>,
96 pub min_price: Option<Price>,
98 pub info: Option<Params>,
100 pub ts_event: UnixNanos,
102 pub ts_init: UnixNanos,
104}
105
106impl CryptoFuture {
107 #[expect(clippy::too_many_arguments)]
116 pub fn new_checked(
117 instrument_id: InstrumentId,
118 raw_symbol: Symbol,
119 underlying: Currency,
120 quote_currency: Currency,
121 settlement_currency: Currency,
122 is_inverse: bool,
123 activation_ns: UnixNanos,
124 expiration_ns: UnixNanos,
125 price_precision: u8,
126 size_precision: u8,
127 price_increment: Price,
128 size_increment: Quantity,
129 multiplier: Option<Quantity>,
130 lot_size: Option<Quantity>,
131 max_quantity: Option<Quantity>,
132 min_quantity: Option<Quantity>,
133 max_notional: Option<Money>,
134 min_notional: Option<Money>,
135 max_price: Option<Price>,
136 min_price: Option<Price>,
137 margin_init: Option<Decimal>,
138 margin_maint: Option<Decimal>,
139 maker_fee: Option<Decimal>,
140 taker_fee: Option<Decimal>,
141 info: Option<Params>,
142 ts_event: UnixNanos,
143 ts_init: UnixNanos,
144 ) -> CorrectnessResult<Self> {
145 check_equal_u8(
146 price_precision,
147 price_increment.precision,
148 stringify!(price_precision),
149 stringify!(price_increment.precision),
150 )?;
151 check_equal_u8(
152 size_precision,
153 size_increment.precision,
154 stringify!(size_precision),
155 stringify!(size_increment.precision),
156 )?;
157 check_positive_price(price_increment, stringify!(price_increment))?;
158 check_positive_quantity(size_increment, stringify!(size_increment))?;
159
160 Ok(Self {
161 id: instrument_id,
162 raw_symbol,
163 underlying,
164 quote_currency,
165 settlement_currency,
166 is_inverse,
167 activation_ns,
168 expiration_ns,
169 price_precision,
170 size_precision,
171 price_increment,
172 size_increment,
173 multiplier: multiplier.unwrap_or(Quantity::from(1)),
174 lot_size: lot_size.unwrap_or(Quantity::from(1)),
175 margin_init: margin_init.unwrap_or_default(),
176 margin_maint: margin_maint.unwrap_or_default(),
177 maker_fee: maker_fee.unwrap_or_default(),
178 taker_fee: taker_fee.unwrap_or_default(),
179 max_quantity,
180 min_quantity,
181 max_notional,
182 min_notional,
183 max_price,
184 min_price,
185 info,
186 ts_event,
187 ts_init,
188 })
189 }
190
191 #[expect(clippy::too_many_arguments)]
197 #[must_use]
198 pub fn new(
199 instrument_id: InstrumentId,
200 raw_symbol: Symbol,
201 underlying: Currency,
202 quote_currency: Currency,
203 settlement_currency: Currency,
204 is_inverse: bool,
205 activation_ns: UnixNanos,
206 expiration_ns: UnixNanos,
207 price_precision: u8,
208 size_precision: u8,
209 price_increment: Price,
210 size_increment: Quantity,
211 multiplier: Option<Quantity>,
212 lot_size: Option<Quantity>,
213 max_quantity: Option<Quantity>,
214 min_quantity: Option<Quantity>,
215 max_notional: Option<Money>,
216 min_notional: Option<Money>,
217 max_price: Option<Price>,
218 min_price: Option<Price>,
219 margin_init: Option<Decimal>,
220 margin_maint: Option<Decimal>,
221 maker_fee: Option<Decimal>,
222 taker_fee: Option<Decimal>,
223 info: Option<Params>,
224 ts_event: UnixNanos,
225 ts_init: UnixNanos,
226 ) -> Self {
227 Self::new_checked(
228 instrument_id,
229 raw_symbol,
230 underlying,
231 quote_currency,
232 settlement_currency,
233 is_inverse,
234 activation_ns,
235 expiration_ns,
236 price_precision,
237 size_precision,
238 price_increment,
239 size_increment,
240 multiplier,
241 lot_size,
242 max_quantity,
243 min_quantity,
244 max_notional,
245 min_notional,
246 max_price,
247 min_price,
248 margin_init,
249 margin_maint,
250 maker_fee,
251 taker_fee,
252 info,
253 ts_event,
254 ts_init,
255 )
256 .expect_display(FAILED)
257 }
258}
259
260impl PartialEq<Self> for CryptoFuture {
261 fn eq(&self, other: &Self) -> bool {
262 self.id == other.id
263 }
264}
265
266impl Eq for CryptoFuture {}
267
268impl Hash for CryptoFuture {
269 fn hash<H: Hasher>(&self, state: &mut H) {
270 self.id.hash(state);
271 }
272}
273
274impl Instrument for CryptoFuture {
275 fn into_any(self) -> InstrumentAny {
276 InstrumentAny::CryptoFuture(self)
277 }
278
279 fn id(&self) -> InstrumentId {
280 self.id
281 }
282
283 fn raw_symbol(&self) -> Symbol {
284 self.raw_symbol
285 }
286
287 fn asset_class(&self) -> AssetClass {
288 AssetClass::Cryptocurrency
289 }
290
291 fn instrument_class(&self) -> InstrumentClass {
292 InstrumentClass::Future
293 }
294
295 fn underlying(&self) -> Option<Ustr> {
296 Some(self.underlying.code)
297 }
298
299 fn base_currency(&self) -> Option<Currency> {
300 Some(self.underlying)
301 }
302
303 fn quote_currency(&self) -> Currency {
304 self.quote_currency
305 }
306
307 fn settlement_currency(&self) -> Currency {
308 self.settlement_currency
309 }
310
311 fn isin(&self) -> Option<Ustr> {
312 None
313 }
314
315 fn exchange(&self) -> Option<Ustr> {
316 None
317 }
318
319 fn option_kind(&self) -> Option<OptionKind> {
320 None
321 }
322
323 fn is_inverse(&self) -> bool {
324 self.is_inverse
325 }
326
327 fn price_precision(&self) -> u8 {
328 self.price_precision
329 }
330
331 fn size_precision(&self) -> u8 {
332 self.size_precision
333 }
334
335 fn price_increment(&self) -> Price {
336 self.price_increment
337 }
338
339 fn size_increment(&self) -> Quantity {
340 self.size_increment
341 }
342
343 fn multiplier(&self) -> Quantity {
344 self.multiplier
345 }
346
347 fn lot_size(&self) -> Option<Quantity> {
348 Some(self.lot_size)
349 }
350
351 fn max_quantity(&self) -> Option<Quantity> {
352 self.max_quantity
353 }
354
355 fn min_quantity(&self) -> Option<Quantity> {
356 self.min_quantity
357 }
358
359 fn max_price(&self) -> Option<Price> {
360 self.max_price
361 }
362
363 fn min_price(&self) -> Option<Price> {
364 self.min_price
365 }
366
367 fn ts_event(&self) -> UnixNanos {
368 self.ts_event
369 }
370
371 fn ts_init(&self) -> UnixNanos {
372 self.ts_init
373 }
374
375 fn margin_init(&self) -> Decimal {
376 self.margin_init
377 }
378
379 fn margin_maint(&self) -> Decimal {
380 self.margin_maint
381 }
382
383 fn maker_fee(&self) -> Decimal {
384 self.maker_fee
385 }
386
387 fn taker_fee(&self) -> Decimal {
388 self.taker_fee
389 }
390
391 fn strike_price(&self) -> Option<Price> {
392 None
393 }
394
395 fn activation_ns(&self) -> Option<UnixNanos> {
396 Some(self.activation_ns)
397 }
398
399 fn expiration_ns(&self) -> Option<UnixNanos> {
400 Some(self.expiration_ns)
401 }
402
403 fn max_notional(&self) -> Option<Money> {
404 self.max_notional
405 }
406
407 fn min_notional(&self) -> Option<Money> {
408 self.min_notional
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use rstest::rstest;
415
416 use crate::{
417 enums::{AssetClass, InstrumentClass},
418 identifiers::{InstrumentId, Symbol},
419 instruments::{CryptoFuture, Instrument, stubs::*},
420 types::{Currency, Price, Quantity},
421 };
422
423 #[rstest]
424 fn test_trait_accessors(crypto_future_btcusdt: CryptoFuture) {
425 assert_eq!(
426 crypto_future_btcusdt.id(),
427 InstrumentId::from("ETHUSDT-123.BINANCE")
428 );
429 assert_eq!(
430 crypto_future_btcusdt.asset_class(),
431 AssetClass::Cryptocurrency
432 );
433 assert_eq!(
434 crypto_future_btcusdt.instrument_class(),
435 InstrumentClass::Future
436 );
437 assert_eq!(crypto_future_btcusdt.quote_currency(), Currency::USDT());
438 assert_eq!(
439 crypto_future_btcusdt.settlement_currency(),
440 Currency::USDT()
441 );
442 assert!(!crypto_future_btcusdt.is_inverse());
443 assert_eq!(crypto_future_btcusdt.price_precision(), 2);
444 assert_eq!(crypto_future_btcusdt.size_precision(), 6);
445 assert!(crypto_future_btcusdt.activation_ns().is_some());
446 assert!(crypto_future_btcusdt.expiration_ns().is_some());
447 }
448
449 #[rstest]
450 fn test_new_checked_price_precision_mismatch() {
451 let result = CryptoFuture::new_checked(
452 InstrumentId::from("TEST.BINANCE"),
453 Symbol::from("TEST"),
454 Currency::BTC(),
455 Currency::USDT(),
456 Currency::USDT(),
457 false,
458 0.into(),
459 0.into(),
460 4, 6,
462 Price::from("0.01"),
463 Quantity::from("0.000001"),
464 None,
465 None,
466 None,
467 None,
468 None,
469 None,
470 None,
471 None,
472 None,
473 None,
474 None,
475 None,
476 None,
477 0.into(),
478 0.into(),
479 );
480 assert!(result.is_err());
481 }
482
483 #[rstest]
484 fn test_serialization_roundtrip(crypto_future_btcusdt: CryptoFuture) {
485 let json = serde_json::to_string(&crypto_future_btcusdt).unwrap();
486 let deserialized: CryptoFuture = serde_json::from_str(&json).unwrap();
487 assert_eq!(crypto_future_btcusdt, deserialized);
488 }
489}