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