1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{CorrectnessResult, check_equal_u8},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny, tick_scheme::check_tick_scheme};
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)]
42#[derive(Clone, Debug, Serialize, Deserialize)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
46)]
47#[cfg_attr(
48 feature = "python",
49 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
50)]
51pub struct Cfd {
52 pub id: InstrumentId,
54 pub raw_symbol: Symbol,
56 pub asset_class: AssetClass,
58 pub base_currency: Option<Currency>,
60 pub quote_currency: Currency,
62 pub price_precision: u8,
64 pub size_precision: u8,
66 pub price_increment: Price,
68 pub size_increment: Quantity,
70 pub margin_init: Decimal,
72 pub margin_maint: Decimal,
74 pub maker_fee: Decimal,
76 pub taker_fee: Decimal,
78 pub lot_size: Option<Quantity>,
80 pub max_quantity: Option<Quantity>,
82 pub min_quantity: Option<Quantity>,
84 pub max_notional: Option<Money>,
86 pub min_notional: Option<Money>,
88 pub max_price: Option<Price>,
90 pub min_price: Option<Price>,
92 pub tick_scheme: Option<Ustr>,
94 pub info: Option<Params>,
96 pub ts_event: UnixNanos,
98 pub ts_init: UnixNanos,
100}
101
102#[bon::bon]
103impl Cfd {
104 #[expect(clippy::too_many_arguments)]
105 fn new_checked(
106 instrument_id: InstrumentId,
107 raw_symbol: Symbol,
108 asset_class: AssetClass,
109 base_currency: Option<Currency>,
110 quote_currency: Currency,
111 price_precision: u8,
112 size_precision: u8,
113 price_increment: Price,
114 size_increment: Quantity,
115 lot_size: Option<Quantity>,
116 max_quantity: Option<Quantity>,
117 min_quantity: Option<Quantity>,
118 max_notional: Option<Money>,
119 min_notional: Option<Money>,
120 max_price: Option<Price>,
121 min_price: Option<Price>,
122 margin_init: Option<Decimal>,
123 margin_maint: Option<Decimal>,
124 maker_fee: Option<Decimal>,
125 taker_fee: Option<Decimal>,
126 tick_scheme: Option<Ustr>,
127 info: Option<Params>,
128 ts_event: UnixNanos,
129 ts_init: UnixNanos,
130 ) -> CorrectnessResult<Self> {
131 check_equal_u8(
132 price_precision,
133 price_increment.precision,
134 stringify!(price_precision),
135 stringify!(price_increment.precision),
136 )?;
137 check_equal_u8(
138 size_precision,
139 size_increment.precision,
140 stringify!(size_precision),
141 stringify!(size_increment.precision),
142 )?;
143 check_positive_price(price_increment, stringify!(price_increment))?;
144 check_positive_quantity(size_increment, stringify!(size_increment))?;
145 check_tick_scheme(tick_scheme)?;
146
147 if let Some(lot_size) = lot_size {
148 check_positive_quantity(lot_size, stringify!(lot_size))?;
149 }
150
151 Ok(Self {
152 id: instrument_id,
153 raw_symbol,
154 asset_class,
155 base_currency,
156 quote_currency,
157 price_precision,
158 size_precision,
159 price_increment,
160 size_increment,
161 lot_size,
162 max_quantity,
163 min_quantity,
164 max_notional,
165 min_notional,
166 max_price,
167 min_price,
168 margin_init: margin_init.unwrap_or_default(),
169 margin_maint: margin_maint.unwrap_or_default(),
170 maker_fee: maker_fee.unwrap_or_default(),
171 taker_fee: taker_fee.unwrap_or_default(),
172 tick_scheme,
173 info,
174 ts_event,
175 ts_init,
176 })
177 }
178
179 #[builder(start_fn = builder, finish_fn = build)]
188 pub fn build_checked(
189 instrument_id: InstrumentId,
190 raw_symbol: Symbol,
191 asset_class: AssetClass,
192 base_currency: Option<Currency>,
193 quote_currency: Currency,
194 price_precision: u8,
195 size_precision: u8,
196 price_increment: Price,
197 size_increment: Quantity,
198 lot_size: Option<Quantity>,
199 max_quantity: Option<Quantity>,
200 min_quantity: Option<Quantity>,
201 max_notional: Option<Money>,
202 min_notional: Option<Money>,
203 max_price: Option<Price>,
204 min_price: Option<Price>,
205 margin_init: Option<Decimal>,
206 margin_maint: Option<Decimal>,
207 maker_fee: Option<Decimal>,
208 taker_fee: Option<Decimal>,
209 tick_scheme: Option<Ustr>,
210 info: Option<Params>,
211 ts_event: UnixNanos,
212 ts_init: UnixNanos,
213 ) -> CorrectnessResult<Self> {
214 Self::new_checked(
215 instrument_id,
216 raw_symbol,
217 asset_class,
218 base_currency,
219 quote_currency,
220 price_precision,
221 size_precision,
222 price_increment,
223 size_increment,
224 lot_size,
225 max_quantity,
226 min_quantity,
227 max_notional,
228 min_notional,
229 max_price,
230 min_price,
231 margin_init,
232 margin_maint,
233 maker_fee,
234 taker_fee,
235 tick_scheme,
236 info,
237 ts_event,
238 ts_init,
239 )
240 }
241}
242
243impl PartialEq<Self> for Cfd {
244 fn eq(&self, other: &Self) -> bool {
245 self.id == other.id
246 }
247}
248
249impl Eq for Cfd {}
250
251impl Hash for Cfd {
252 fn hash<H: Hasher>(&self, state: &mut H) {
253 self.id.hash(state);
254 }
255}
256
257impl Instrument for Cfd {
258 fn tick_scheme(&self) -> Option<Ustr> {
259 self.tick_scheme
260 }
261 fn into_any(self) -> InstrumentAny {
262 InstrumentAny::Cfd(self)
263 }
264
265 fn id(&self) -> InstrumentId {
266 self.id
267 }
268
269 fn raw_symbol(&self) -> Symbol {
270 self.raw_symbol
271 }
272
273 fn asset_class(&self) -> AssetClass {
274 self.asset_class
275 }
276
277 fn instrument_class(&self) -> InstrumentClass {
278 InstrumentClass::Cfd
279 }
280
281 fn underlying(&self) -> Option<Ustr> {
282 None
283 }
284
285 fn base_currency(&self) -> Option<Currency> {
286 self.base_currency
287 }
288
289 fn quote_currency(&self) -> Currency {
290 self.quote_currency
291 }
292
293 fn settlement_currency(&self) -> Currency {
294 self.quote_currency
295 }
296
297 fn isin(&self) -> Option<Ustr> {
298 None
299 }
300
301 fn option_kind(&self) -> Option<OptionKind> {
302 None
303 }
304
305 fn exchange(&self) -> Option<Ustr> {
306 None
307 }
308
309 fn strike_price(&self) -> Option<Price> {
310 None
311 }
312
313 fn activation_ns(&self) -> Option<UnixNanos> {
314 None
315 }
316
317 fn expiration_ns(&self) -> Option<UnixNanos> {
318 None
319 }
320
321 fn is_inverse(&self) -> bool {
322 false
323 }
324
325 fn price_precision(&self) -> u8 {
326 self.price_precision
327 }
328
329 fn size_precision(&self) -> u8 {
330 self.size_precision
331 }
332
333 fn price_increment(&self) -> Price {
334 self.price_increment
335 }
336
337 fn size_increment(&self) -> Quantity {
338 self.size_increment
339 }
340
341 fn multiplier(&self) -> Quantity {
342 Quantity::from(1)
343 }
344
345 fn lot_size(&self) -> Option<Quantity> {
346 self.lot_size
347 }
348
349 fn max_quantity(&self) -> Option<Quantity> {
350 self.max_quantity
351 }
352
353 fn min_quantity(&self) -> Option<Quantity> {
354 self.min_quantity
355 }
356
357 fn max_notional(&self) -> Option<Money> {
358 self.max_notional
359 }
360
361 fn min_notional(&self) -> Option<Money> {
362 self.min_notional
363 }
364
365 fn max_price(&self) -> Option<Price> {
366 self.max_price
367 }
368
369 fn min_price(&self) -> Option<Price> {
370 self.min_price
371 }
372
373 fn margin_init(&self) -> Decimal {
374 self.margin_init
375 }
376
377 fn margin_maint(&self) -> Decimal {
378 self.margin_maint
379 }
380
381 fn maker_fee(&self) -> Decimal {
382 self.maker_fee
383 }
384
385 fn taker_fee(&self) -> Decimal {
386 self.taker_fee
387 }
388
389 fn ts_event(&self) -> UnixNanos {
390 self.ts_event
391 }
392
393 fn ts_init(&self) -> UnixNanos {
394 self.ts_init
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use rstest::rstest;
401 use rust_decimal_macros::dec;
402
403 use crate::{
404 enums::{AssetClass, InstrumentClass},
405 identifiers::{InstrumentId, Symbol},
406 instruments::{Cfd, Instrument, stubs::*},
407 types::{Currency, Money, Price, Quantity},
408 };
409
410 #[rstest]
411 fn test_trait_accessors(cfd_gold: Cfd) {
412 assert_eq!(cfd_gold.id(), InstrumentId::from("GOLD-CFD.SIM"));
413 assert_eq!(cfd_gold.asset_class(), AssetClass::Commodity);
414 assert_eq!(cfd_gold.instrument_class(), InstrumentClass::Cfd);
415 assert_eq!(cfd_gold.quote_currency(), Currency::USD());
416 assert!(!cfd_gold.is_inverse());
417 assert_eq!(cfd_gold.price_precision(), 2);
418 assert_eq!(cfd_gold.size_precision(), 0);
419 }
420
421 #[rstest]
422 fn test_new_checked_price_precision_mismatch() {
423 let result = Cfd::new_checked(
424 InstrumentId::from("TEST.SIM"),
425 Symbol::from("TEST"),
426 AssetClass::Commodity,
427 None,
428 Currency::USD(),
429 4, 0,
431 Price::from("0.01"),
432 Quantity::from("1"),
433 None,
434 None,
435 None,
436 None,
437 None,
438 None,
439 None,
440 None,
441 None,
442 None,
443 None,
444 None,
445 None,
446 0.into(),
447 0.into(),
448 );
449 assert!(result.is_err());
450 }
451
452 #[rstest]
453 fn test_new_checked_rejects_non_positive_lot_size() {
454 let result = Cfd::new_checked(
455 InstrumentId::from("TEST.SIM"),
456 Symbol::from("TEST"),
457 AssetClass::Commodity,
458 None,
459 Currency::USD(),
460 2,
461 0,
462 Price::from("0.01"),
463 Quantity::from("1"),
464 Some(Quantity::from("0")),
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 let error = result.unwrap_err();
481 assert!(error.to_string().contains("not positive"), "{error}");
482 }
483
484 #[rstest]
485 fn test_serialization_roundtrip(cfd_gold: Cfd) {
486 let json = serde_json::to_string(&cfd_gold).unwrap();
487 let deserialized: Cfd = serde_json::from_str(&json).unwrap();
488 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
489 }
490
491 #[rstest]
492 fn test_builder_matches_new_checked() {
493 let positional = Cfd::new_checked(
494 InstrumentId::from("EURUSD-CFD.SIM"),
495 Symbol::from("EURUSD-CFD"),
496 AssetClass::FX,
497 Some(Currency::EUR()),
498 Currency::USD(),
499 5,
500 2,
501 Price::from("0.00001"),
502 Quantity::from("0.01"),
503 Some(Quantity::from("100")),
504 Some(Quantity::from("10000.00")),
505 Some(Quantity::from("5.00")),
506 Some(Money::from("1000000 USD")),
507 Some(Money::from("100 USD")),
508 Some(Price::from("2.00000")),
509 Some(Price::from("0.50000")),
510 Some(dec!(0.01)),
511 Some(dec!(0.02)),
512 Some(dec!(0.0002)),
513 Some(dec!(0.0004)),
514 None,
515 None,
516 1.into(),
517 2.into(),
518 )
519 .unwrap();
520
521 let built = Cfd::builder()
522 .instrument_id(InstrumentId::from("EURUSD-CFD.SIM"))
523 .raw_symbol(Symbol::from("EURUSD-CFD"))
524 .asset_class(AssetClass::FX)
525 .base_currency(Currency::EUR())
526 .quote_currency(Currency::USD())
527 .price_precision(5)
528 .size_precision(2)
529 .price_increment(Price::from("0.00001"))
530 .size_increment(Quantity::from("0.01"))
531 .lot_size(Quantity::from("100"))
532 .max_quantity(Quantity::from("10000.00"))
533 .min_quantity(Quantity::from("5.00"))
534 .max_notional(Money::from("1000000 USD"))
535 .min_notional(Money::from("100 USD"))
536 .max_price(Price::from("2.00000"))
537 .min_price(Price::from("0.50000"))
538 .margin_init(dec!(0.01))
539 .margin_maint(dec!(0.02))
540 .maker_fee(dec!(0.0002))
541 .taker_fee(dec!(0.0004))
542 .ts_event(1.into())
543 .ts_init(2.into())
544 .build()
545 .unwrap();
546
547 assert_eq!(
548 serde_json::to_value(&positional).unwrap(),
549 serde_json::to_value(&built).unwrap(),
550 );
551 }
552}