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 into_any(self) -> InstrumentAny {
259 InstrumentAny::Cfd(self)
260 }
261
262 fn id(&self) -> InstrumentId {
263 self.id
264 }
265
266 fn raw_symbol(&self) -> Symbol {
267 self.raw_symbol
268 }
269
270 fn asset_class(&self) -> AssetClass {
271 self.asset_class
272 }
273
274 fn instrument_class(&self) -> InstrumentClass {
275 InstrumentClass::Cfd
276 }
277
278 fn underlying(&self) -> Option<Ustr> {
279 None
280 }
281
282 fn base_currency(&self) -> Option<Currency> {
283 self.base_currency
284 }
285
286 fn quote_currency(&self) -> Currency {
287 self.quote_currency
288 }
289
290 fn settlement_currency(&self) -> Currency {
291 self.quote_currency
292 }
293
294 fn isin(&self) -> Option<Ustr> {
295 None
296 }
297
298 fn option_kind(&self) -> Option<OptionKind> {
299 None
300 }
301
302 fn exchange(&self) -> Option<Ustr> {
303 None
304 }
305
306 fn strike_price(&self) -> Option<Price> {
307 None
308 }
309
310 fn activation_ns(&self) -> Option<UnixNanos> {
311 None
312 }
313
314 fn expiration_ns(&self) -> Option<UnixNanos> {
315 None
316 }
317
318 fn is_inverse(&self) -> bool {
319 false
320 }
321
322 fn price_precision(&self) -> u8 {
323 self.price_precision
324 }
325
326 fn size_precision(&self) -> u8 {
327 self.size_precision
328 }
329
330 fn price_increment(&self) -> Price {
331 self.price_increment
332 }
333
334 fn size_increment(&self) -> Quantity {
335 self.size_increment
336 }
337
338 fn multiplier(&self) -> Quantity {
339 Quantity::from(1)
340 }
341
342 fn lot_size(&self) -> Option<Quantity> {
343 self.lot_size
344 }
345
346 fn max_quantity(&self) -> Option<Quantity> {
347 self.max_quantity
348 }
349
350 fn min_quantity(&self) -> Option<Quantity> {
351 self.min_quantity
352 }
353
354 fn max_notional(&self) -> Option<Money> {
355 self.max_notional
356 }
357
358 fn min_notional(&self) -> Option<Money> {
359 self.min_notional
360 }
361
362 fn max_price(&self) -> Option<Price> {
363 self.max_price
364 }
365
366 fn min_price(&self) -> Option<Price> {
367 self.min_price
368 }
369
370 fn margin_init(&self) -> Decimal {
371 self.margin_init
372 }
373
374 fn margin_maint(&self) -> Decimal {
375 self.margin_maint
376 }
377
378 fn maker_fee(&self) -> Decimal {
379 self.maker_fee
380 }
381
382 fn taker_fee(&self) -> Decimal {
383 self.taker_fee
384 }
385
386 fn tick_scheme(&self) -> Option<Ustr> {
387 self.tick_scheme
388 }
389
390 fn info(&self) -> Option<&Params> {
391 self.info.as_ref()
392 }
393
394 fn ts_event(&self) -> UnixNanos {
395 self.ts_event
396 }
397
398 fn ts_init(&self) -> UnixNanos {
399 self.ts_init
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use rstest::rstest;
406 use rust_decimal_macros::dec;
407
408 use crate::{
409 enums::{AssetClass, InstrumentClass},
410 identifiers::{InstrumentId, Symbol},
411 instruments::{Cfd, Instrument, stubs::*},
412 types::{Currency, Money, Price, Quantity},
413 };
414
415 #[rstest]
416 fn test_trait_accessors(cfd_gold: Cfd) {
417 assert_eq!(cfd_gold.id(), InstrumentId::from("GOLD-CFD.SIM"));
418 assert_eq!(cfd_gold.asset_class(), AssetClass::Commodity);
419 assert_eq!(cfd_gold.instrument_class(), InstrumentClass::Cfd);
420 assert_eq!(cfd_gold.quote_currency(), Currency::USD());
421 assert!(!cfd_gold.is_inverse());
422 assert_eq!(cfd_gold.price_precision(), 2);
423 assert_eq!(cfd_gold.size_precision(), 0);
424 }
425
426 #[rstest]
427 fn test_new_checked_price_precision_mismatch() {
428 let result = Cfd::new_checked(
429 InstrumentId::from("TEST.SIM"),
430 Symbol::from("TEST"),
431 AssetClass::Commodity,
432 None,
433 Currency::USD(),
434 4, 0,
436 Price::from("0.01"),
437 Quantity::from("1"),
438 None,
439 None,
440 None,
441 None,
442 None,
443 None,
444 None,
445 None,
446 None,
447 None,
448 None,
449 None,
450 None,
451 0.into(),
452 0.into(),
453 );
454 assert!(result.is_err());
455 }
456
457 #[rstest]
458 fn test_new_checked_rejects_non_positive_lot_size() {
459 let result = Cfd::new_checked(
460 InstrumentId::from("TEST.SIM"),
461 Symbol::from("TEST"),
462 AssetClass::Commodity,
463 None,
464 Currency::USD(),
465 2,
466 0,
467 Price::from("0.01"),
468 Quantity::from("1"),
469 Some(Quantity::from("0")),
470 None,
471 None,
472 None,
473 None,
474 None,
475 None,
476 None,
477 None,
478 None,
479 None,
480 None,
481 None,
482 0.into(),
483 0.into(),
484 );
485 let error = result.unwrap_err();
486 assert!(error.to_string().contains("not positive"), "{error}");
487 }
488
489 #[rstest]
490 fn test_serialization_roundtrip(cfd_gold: Cfd) {
491 let json = serde_json::to_string(&cfd_gold).unwrap();
492 let deserialized: Cfd = serde_json::from_str(&json).unwrap();
493 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
494 }
495
496 #[rstest]
497 fn test_builder_matches_new_checked() {
498 let positional = Cfd::new_checked(
499 InstrumentId::from("EURUSD-CFD.SIM"),
500 Symbol::from("EURUSD-CFD"),
501 AssetClass::FX,
502 Some(Currency::EUR()),
503 Currency::USD(),
504 5,
505 2,
506 Price::from("0.00001"),
507 Quantity::from("0.01"),
508 Some(Quantity::from("100")),
509 Some(Quantity::from("10000.00")),
510 Some(Quantity::from("5.00")),
511 Some(Money::from("1000000 USD")),
512 Some(Money::from("100 USD")),
513 Some(Price::from("2.00000")),
514 Some(Price::from("0.50000")),
515 Some(dec!(0.01)),
516 Some(dec!(0.02)),
517 Some(dec!(0.0002)),
518 Some(dec!(0.0004)),
519 None,
520 None,
521 1.into(),
522 2.into(),
523 )
524 .unwrap();
525
526 let built = Cfd::builder()
527 .instrument_id(InstrumentId::from("EURUSD-CFD.SIM"))
528 .raw_symbol(Symbol::from("EURUSD-CFD"))
529 .asset_class(AssetClass::FX)
530 .base_currency(Currency::EUR())
531 .quote_currency(Currency::USD())
532 .price_precision(5)
533 .size_precision(2)
534 .price_increment(Price::from("0.00001"))
535 .size_increment(Quantity::from("0.01"))
536 .lot_size(Quantity::from("100"))
537 .max_quantity(Quantity::from("10000.00"))
538 .min_quantity(Quantity::from("5.00"))
539 .max_notional(Money::from("1000000 USD"))
540 .min_notional(Money::from("100 USD"))
541 .max_price(Price::from("2.00000"))
542 .min_price(Price::from("0.50000"))
543 .margin_init(dec!(0.01))
544 .margin_maint(dec!(0.02))
545 .maker_fee(dec!(0.0002))
546 .taker_fee(dec!(0.0004))
547 .ts_event(1.into())
548 .ts_init(2.into())
549 .build()
550 .unwrap();
551
552 assert_eq!(
553 serde_json::to_value(&positional).unwrap(),
554 serde_json::to_value(&built).unwrap(),
555 );
556 }
557}