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)]
42#[derive(Clone, Debug, Serialize, Deserialize)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.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 info: Option<Params>,
94 pub ts_event: UnixNanos,
96 pub ts_init: UnixNanos,
98}
99
100impl Cfd {
101 #[expect(clippy::too_many_arguments)]
110 pub fn new_checked(
111 instrument_id: InstrumentId,
112 raw_symbol: Symbol,
113 asset_class: AssetClass,
114 base_currency: Option<Currency>,
115 quote_currency: Currency,
116 price_precision: u8,
117 size_precision: u8,
118 price_increment: Price,
119 size_increment: Quantity,
120 lot_size: Option<Quantity>,
121 max_quantity: Option<Quantity>,
122 min_quantity: Option<Quantity>,
123 max_notional: Option<Money>,
124 min_notional: Option<Money>,
125 max_price: Option<Price>,
126 min_price: Option<Price>,
127 margin_init: Option<Decimal>,
128 margin_maint: Option<Decimal>,
129 maker_fee: Option<Decimal>,
130 taker_fee: Option<Decimal>,
131 info: Option<Params>,
132 ts_event: UnixNanos,
133 ts_init: UnixNanos,
134 ) -> CorrectnessResult<Self> {
135 check_equal_u8(
136 price_precision,
137 price_increment.precision,
138 stringify!(price_precision),
139 stringify!(price_increment.precision),
140 )?;
141 check_equal_u8(
142 size_precision,
143 size_increment.precision,
144 stringify!(size_precision),
145 stringify!(size_increment.precision),
146 )?;
147 check_positive_price(price_increment, stringify!(price_increment))?;
148 check_positive_quantity(size_increment, stringify!(size_increment))?;
149
150 Ok(Self {
151 id: instrument_id,
152 raw_symbol,
153 asset_class,
154 base_currency,
155 quote_currency,
156 price_precision,
157 size_precision,
158 price_increment,
159 size_increment,
160 lot_size,
161 max_quantity,
162 min_quantity,
163 max_notional,
164 min_notional,
165 max_price,
166 min_price,
167 margin_init: margin_init.unwrap_or_default(),
168 margin_maint: margin_maint.unwrap_or_default(),
169 maker_fee: maker_fee.unwrap_or_default(),
170 taker_fee: taker_fee.unwrap_or_default(),
171 info,
172 ts_event,
173 ts_init,
174 })
175 }
176
177 #[expect(clippy::too_many_arguments)]
183 #[must_use]
184 pub fn new(
185 instrument_id: InstrumentId,
186 raw_symbol: Symbol,
187 asset_class: AssetClass,
188 base_currency: Option<Currency>,
189 quote_currency: Currency,
190 price_precision: u8,
191 size_precision: u8,
192 price_increment: Price,
193 size_increment: Quantity,
194 lot_size: Option<Quantity>,
195 max_quantity: Option<Quantity>,
196 min_quantity: Option<Quantity>,
197 max_notional: Option<Money>,
198 min_notional: Option<Money>,
199 max_price: Option<Price>,
200 min_price: Option<Price>,
201 margin_init: Option<Decimal>,
202 margin_maint: Option<Decimal>,
203 maker_fee: Option<Decimal>,
204 taker_fee: Option<Decimal>,
205 info: Option<Params>,
206 ts_event: UnixNanos,
207 ts_init: UnixNanos,
208 ) -> Self {
209 Self::new_checked(
210 instrument_id,
211 raw_symbol,
212 asset_class,
213 base_currency,
214 quote_currency,
215 price_precision,
216 size_precision,
217 price_increment,
218 size_increment,
219 lot_size,
220 max_quantity,
221 min_quantity,
222 max_notional,
223 min_notional,
224 max_price,
225 min_price,
226 margin_init,
227 margin_maint,
228 maker_fee,
229 taker_fee,
230 info,
231 ts_event,
232 ts_init,
233 )
234 .expect_display(FAILED)
235 }
236}
237
238impl PartialEq<Self> for Cfd {
239 fn eq(&self, other: &Self) -> bool {
240 self.id == other.id
241 }
242}
243
244impl Eq for Cfd {}
245
246impl Hash for Cfd {
247 fn hash<H: Hasher>(&self, state: &mut H) {
248 self.id.hash(state);
249 }
250}
251
252impl Instrument for Cfd {
253 fn into_any(self) -> InstrumentAny {
254 InstrumentAny::Cfd(self)
255 }
256
257 fn id(&self) -> InstrumentId {
258 self.id
259 }
260
261 fn raw_symbol(&self) -> Symbol {
262 self.raw_symbol
263 }
264
265 fn asset_class(&self) -> AssetClass {
266 self.asset_class
267 }
268
269 fn instrument_class(&self) -> InstrumentClass {
270 InstrumentClass::Cfd
271 }
272
273 fn underlying(&self) -> Option<Ustr> {
274 None
275 }
276
277 fn base_currency(&self) -> Option<Currency> {
278 self.base_currency
279 }
280
281 fn quote_currency(&self) -> Currency {
282 self.quote_currency
283 }
284
285 fn settlement_currency(&self) -> Currency {
286 self.quote_currency
287 }
288
289 fn isin(&self) -> Option<Ustr> {
290 None
291 }
292
293 fn option_kind(&self) -> Option<OptionKind> {
294 None
295 }
296
297 fn exchange(&self) -> Option<Ustr> {
298 None
299 }
300
301 fn strike_price(&self) -> Option<Price> {
302 None
303 }
304
305 fn activation_ns(&self) -> Option<UnixNanos> {
306 None
307 }
308
309 fn expiration_ns(&self) -> Option<UnixNanos> {
310 None
311 }
312
313 fn is_inverse(&self) -> bool {
314 false
315 }
316
317 fn price_precision(&self) -> u8 {
318 self.price_precision
319 }
320
321 fn size_precision(&self) -> u8 {
322 self.size_precision
323 }
324
325 fn price_increment(&self) -> Price {
326 self.price_increment
327 }
328
329 fn size_increment(&self) -> Quantity {
330 self.size_increment
331 }
332
333 fn multiplier(&self) -> Quantity {
334 Quantity::from(1)
335 }
336
337 fn lot_size(&self) -> Option<Quantity> {
338 self.lot_size
339 }
340
341 fn max_quantity(&self) -> Option<Quantity> {
342 self.max_quantity
343 }
344
345 fn min_quantity(&self) -> Option<Quantity> {
346 self.min_quantity
347 }
348
349 fn max_notional(&self) -> Option<Money> {
350 self.max_notional
351 }
352
353 fn min_notional(&self) -> Option<Money> {
354 self.min_notional
355 }
356
357 fn max_price(&self) -> Option<Price> {
358 self.max_price
359 }
360
361 fn min_price(&self) -> Option<Price> {
362 self.min_price
363 }
364
365 fn margin_init(&self) -> Decimal {
366 self.margin_init
367 }
368
369 fn margin_maint(&self) -> Decimal {
370 self.margin_maint
371 }
372
373 fn maker_fee(&self) -> Decimal {
374 self.maker_fee
375 }
376
377 fn taker_fee(&self) -> Decimal {
378 self.taker_fee
379 }
380
381 fn ts_event(&self) -> UnixNanos {
382 self.ts_event
383 }
384
385 fn ts_init(&self) -> UnixNanos {
386 self.ts_init
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use rstest::rstest;
393
394 use crate::{
395 enums::{AssetClass, InstrumentClass},
396 identifiers::{InstrumentId, Symbol},
397 instruments::{Cfd, Instrument, stubs::*},
398 types::{Currency, Price, Quantity},
399 };
400
401 #[rstest]
402 fn test_trait_accessors(cfd_gold: Cfd) {
403 assert_eq!(cfd_gold.id(), InstrumentId::from("GOLD-CFD.SIM"));
404 assert_eq!(cfd_gold.asset_class(), AssetClass::Commodity);
405 assert_eq!(cfd_gold.instrument_class(), InstrumentClass::Cfd);
406 assert_eq!(cfd_gold.quote_currency(), Currency::USD());
407 assert!(!cfd_gold.is_inverse());
408 assert_eq!(cfd_gold.price_precision(), 2);
409 assert_eq!(cfd_gold.size_precision(), 0);
410 }
411
412 #[rstest]
413 fn test_new_checked_price_precision_mismatch() {
414 let result = Cfd::new_checked(
415 InstrumentId::from("TEST.SIM"),
416 Symbol::from("TEST"),
417 AssetClass::Commodity,
418 None,
419 Currency::USD(),
420 4, 0,
422 Price::from("0.01"),
423 Quantity::from("1"),
424 None,
425 None,
426 None,
427 None,
428 None,
429 None,
430 None,
431 None,
432 None,
433 None,
434 None,
435 None,
436 0.into(),
437 0.into(),
438 );
439 assert!(result.is_err());
440 }
441
442 #[rstest]
443 fn test_serialization_roundtrip(cfd_gold: Cfd) {
444 let json = serde_json::to_string(&cfd_gold).unwrap();
445 let deserialized: Cfd = serde_json::from_str(&json).unwrap();
446 assert_eq!(cfd_gold, deserialized);
447 }
448}