1#![allow(dead_code)]
20
21use std::{
22 collections::HashMap,
23 fmt::Display,
24 hash::{Hash, Hasher},
25 ops::{Deref, DerefMut},
26};
27
28use rust_decimal::prelude::ToPrimitive;
29use serde::{Deserialize, Serialize};
30
31use crate::{
32 accounts::{Account, base::BaseAccount},
33 enums::{AccountType, LiquiditySide, OrderSide},
34 events::{AccountState, OrderFilled},
35 identifiers::{AccountId, InstrumentId},
36 instruments::{Instrument, InstrumentAny},
37 position::Position,
38 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
39};
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct MarginAccount {
47 pub base: BaseAccount,
48 pub leverages: HashMap<InstrumentId, f64>,
49 pub margins: HashMap<InstrumentId, MarginBalance>,
50 pub default_leverage: f64,
51}
52
53impl MarginAccount {
54 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
56 Self {
57 base: BaseAccount::new(event, calculate_account_state),
58 leverages: HashMap::new(),
59 margins: HashMap::new(),
60 default_leverage: 1.0,
61 }
62 }
63
64 pub fn set_default_leverage(&mut self, leverage: f64) {
65 self.default_leverage = leverage;
66 }
67
68 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: f64) {
69 self.leverages.insert(instrument_id, leverage);
70 }
71
72 #[must_use]
73 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> f64 {
74 *self
75 .leverages
76 .get(instrument_id)
77 .unwrap_or(&self.default_leverage)
78 }
79
80 #[must_use]
81 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
82 self.get_leverage(&instrument_id) == 1.0
83 }
84
85 #[must_use]
86 pub fn is_cash_account(&self) -> bool {
87 self.account_type == AccountType::Cash
88 }
89 #[must_use]
90 pub fn is_margin_account(&self) -> bool {
91 self.account_type == AccountType::Margin
92 }
93
94 #[must_use]
95 pub fn initial_margins(&self) -> HashMap<InstrumentId, Money> {
96 let mut initial_margins: HashMap<InstrumentId, Money> = HashMap::new();
97 self.margins.values().for_each(|margin_balance| {
98 initial_margins.insert(margin_balance.instrument_id, margin_balance.initial);
99 });
100 initial_margins
101 }
102
103 #[must_use]
104 pub fn maintenance_margins(&self) -> HashMap<InstrumentId, Money> {
105 let mut maintenance_margins: HashMap<InstrumentId, Money> = HashMap::new();
106 self.margins.values().for_each(|margin_balance| {
107 maintenance_margins.insert(margin_balance.instrument_id, margin_balance.maintenance);
108 });
109 maintenance_margins
110 }
111
112 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
118 let margin_balance = self.margins.get(&instrument_id);
119 if margin_balance.is_none() {
120 self.margins.insert(
121 instrument_id,
122 MarginBalance::new(
123 margin_init,
124 Money::new(0.0, margin_init.currency),
125 instrument_id,
126 ),
127 );
128 } else {
129 let mut new_margin_balance = *margin_balance.unwrap();
131 new_margin_balance.initial = margin_init;
132 self.margins.insert(instrument_id, new_margin_balance);
133 }
134 self.recalculate_balance(margin_init.currency);
135 }
136
137 #[must_use]
143 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
144 let margin_balance = self.margins.get(&instrument_id);
145 assert!(
146 margin_balance.is_some(),
147 "Cannot get margin_init when no margin_balance"
148 );
149 margin_balance.unwrap().initial
150 }
151
152 pub fn update_maintenance_margin(
158 &mut self,
159 instrument_id: InstrumentId,
160 margin_maintenance: Money,
161 ) {
162 let margin_balance = self.margins.get(&instrument_id);
163 if margin_balance.is_none() {
164 self.margins.insert(
165 instrument_id,
166 MarginBalance::new(
167 Money::new(0.0, margin_maintenance.currency),
168 margin_maintenance,
169 instrument_id,
170 ),
171 );
172 } else {
173 let mut new_margin_balance = *margin_balance.unwrap();
175 new_margin_balance.maintenance = margin_maintenance;
176 self.margins.insert(instrument_id, new_margin_balance);
177 }
178 self.recalculate_balance(margin_maintenance.currency);
179 }
180
181 #[must_use]
187 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
188 let margin_balance = self.margins.get(&instrument_id);
189 assert!(
190 margin_balance.is_some(),
191 "Cannot get maintenance_margin when no margin_balance"
192 );
193 margin_balance.unwrap().maintenance
194 }
195
196 pub fn calculate_initial_margin<T: Instrument>(
202 &mut self,
203 instrument: T,
204 quantity: Quantity,
205 price: Price,
206 use_quote_for_inverse: Option<bool>,
207 ) -> Money {
208 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
209 let leverage = self.get_leverage(&instrument.id());
210 if leverage == 0.0 {
211 self.leverages
212 .insert(instrument.id(), self.default_leverage);
213 }
214 let adjusted_notional = notional / leverage;
215 let initial_margin_f64 = instrument.margin_init().to_f64().unwrap();
216 let margin = adjusted_notional * initial_margin_f64;
217
218 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
219 if instrument.is_inverse() && !use_quote_for_inverse {
220 Money::new(margin, instrument.base_currency().unwrap())
221 } else {
222 Money::new(margin, instrument.quote_currency())
223 }
224 }
225
226 pub fn calculate_maintenance_margin<T: Instrument>(
232 &mut self,
233 instrument: T,
234 quantity: Quantity,
235 price: Price,
236 use_quote_for_inverse: Option<bool>,
237 ) -> Money {
238 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
239 let leverage = self.get_leverage(&instrument.id());
240 if leverage == 0.0 {
241 self.leverages
242 .insert(instrument.id(), self.default_leverage);
243 }
244 let adjusted_notional = notional / leverage;
245 let margin_maint_f64 = instrument.margin_maint().to_f64().unwrap();
246 let margin = adjusted_notional * margin_maint_f64;
247
248 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
249 if instrument.is_inverse() && !use_quote_for_inverse {
250 Money::new(margin, instrument.base_currency().unwrap())
251 } else {
252 Money::new(margin, instrument.quote_currency())
253 }
254 }
255
256 pub fn recalculate_balance(&mut self, currency: Currency) {
264 let current_balance = match self.balances.get(¤cy) {
265 Some(balance) => balance,
266 None => panic!("Cannot recalculate balance when no starting balance"),
267 };
268
269 let mut total_margin = 0;
270 self.margins.values().for_each(|margin| {
272 if margin.currency == currency {
273 total_margin += margin.initial.raw;
274 total_margin += margin.maintenance.raw;
275 }
276 });
277 let total_free = current_balance.total.raw - total_margin;
278 assert!(
280 total_free >= 0,
281 "Cannot recalculate balance when total_free is less than 0.0"
282 );
283 let new_balance = AccountBalance::new(
284 current_balance.total,
285 Money::from_raw(total_margin, currency),
286 Money::from_raw(total_free, currency),
287 );
288 self.balances.insert(currency, new_balance);
289 }
290}
291
292impl Deref for MarginAccount {
293 type Target = BaseAccount;
294
295 fn deref(&self) -> &Self::Target {
296 &self.base
297 }
298}
299
300impl DerefMut for MarginAccount {
301 fn deref_mut(&mut self) -> &mut Self::Target {
302 &mut self.base
303 }
304}
305
306impl Account for MarginAccount {
307 fn id(&self) -> AccountId {
308 self.id
309 }
310
311 fn account_type(&self) -> AccountType {
312 self.account_type
313 }
314
315 fn base_currency(&self) -> Option<Currency> {
316 self.base_currency
317 }
318
319 fn is_cash_account(&self) -> bool {
320 self.account_type == AccountType::Cash
321 }
322
323 fn is_margin_account(&self) -> bool {
324 self.account_type == AccountType::Margin
325 }
326
327 fn calculated_account_state(&self) -> bool {
328 false }
330
331 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
332 self.base_balance_total(currency)
333 }
334
335 fn balances_total(&self) -> HashMap<Currency, Money> {
336 self.base_balances_total()
337 }
338
339 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
340 self.base_balance_free(currency)
341 }
342
343 fn balances_free(&self) -> HashMap<Currency, Money> {
344 self.base_balances_free()
345 }
346
347 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
348 self.base_balance_locked(currency)
349 }
350
351 fn balances_locked(&self) -> HashMap<Currency, Money> {
352 self.base_balances_locked()
353 }
354
355 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
356 self.base_balance(currency)
357 }
358
359 fn last_event(&self) -> Option<AccountState> {
360 self.base_last_event()
361 }
362
363 fn events(&self) -> Vec<AccountState> {
364 self.events.clone()
365 }
366
367 fn event_count(&self) -> usize {
368 self.events.len()
369 }
370
371 fn currencies(&self) -> Vec<Currency> {
372 self.balances.keys().copied().collect()
373 }
374
375 fn starting_balances(&self) -> HashMap<Currency, Money> {
376 self.balances_starting.clone()
377 }
378
379 fn balances(&self) -> HashMap<Currency, AccountBalance> {
380 self.balances.clone()
381 }
382
383 fn apply(&mut self, event: AccountState) {
384 self.base_apply(event);
385 }
386
387 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
388 self.base.base_purge_account_events(ts_now, lookback_secs);
389 }
390
391 fn calculate_balance_locked(
392 &mut self,
393 instrument: InstrumentAny,
394 side: OrderSide,
395 quantity: Quantity,
396 price: Price,
397 use_quote_for_inverse: Option<bool>,
398 ) -> anyhow::Result<Money> {
399 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
400 }
401
402 fn calculate_pnls(
403 &self,
404 _instrument: InstrumentAny, fill: OrderFilled,
406 position: Option<Position>,
407 ) -> anyhow::Result<Vec<Money>> {
408 let mut pnls: Vec<Money> = Vec::new();
409
410 if let Some(ref pos) = position {
411 if pos.quantity.is_positive() && pos.entry != fill.order_side {
412 let pnl_quantity = Quantity::from_raw(
415 fill.last_qty.raw.min(pos.quantity.raw),
416 fill.last_qty.precision,
417 );
418 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
419 pnls.push(pnl);
420 }
421 }
422
423 Ok(pnls)
424 }
425
426 fn calculate_commission(
427 &self,
428 instrument: InstrumentAny,
429 last_qty: Quantity,
430 last_px: Price,
431 liquidity_side: LiquiditySide,
432 use_quote_for_inverse: Option<bool>,
433 ) -> anyhow::Result<Money> {
434 self.base_calculate_commission(
435 instrument,
436 last_qty,
437 last_px,
438 liquidity_side,
439 use_quote_for_inverse,
440 )
441 }
442}
443
444impl PartialEq for MarginAccount {
445 fn eq(&self, other: &Self) -> bool {
446 self.id == other.id
447 }
448}
449
450impl Eq for MarginAccount {}
451
452impl Display for MarginAccount {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 write!(
455 f,
456 "MarginAccount(id={}, type={}, base={})",
457 self.id,
458 self.account_type,
459 self.base_currency.map_or_else(
460 || "None".to_string(),
461 |base_currency| format!("{}", base_currency.code)
462 ),
463 )
464 }
465}
466
467impl Hash for MarginAccount {
468 fn hash<H: Hasher>(&self, state: &mut H) {
469 self.id.hash(state);
470 }
471}
472
473#[cfg(test)]
477mod tests {
478 use std::collections::HashMap;
479
480 use nautilus_core::UnixNanos;
481 use rstest::rstest;
482
483 use crate::{
484 accounts::{Account, MarginAccount, stubs::*},
485 enums::{LiquiditySide, OrderSide, OrderType},
486 events::{AccountState, OrderFilled, account::stubs::*},
487 identifiers::{
488 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
489 VenueOrderId,
490 stubs::{uuid4, *},
491 },
492 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
493 position::Position,
494 types::{Currency, Money, Price, Quantity},
495 };
496
497 #[rstest]
498 fn test_display(margin_account: MarginAccount) {
499 assert_eq!(
500 margin_account.to_string(),
501 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
502 );
503 }
504
505 #[rstest]
506 fn test_base_account_properties(
507 margin_account: MarginAccount,
508 margin_account_state: AccountState,
509 ) {
510 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
511 assert_eq!(
512 margin_account.last_event(),
513 Some(margin_account_state.clone())
514 );
515 assert_eq!(margin_account.events(), vec![margin_account_state]);
516 assert_eq!(margin_account.event_count(), 1);
517 assert_eq!(
518 margin_account.balance_total(None),
519 Some(Money::from("1525000 USD"))
520 );
521 assert_eq!(
522 margin_account.balance_free(None),
523 Some(Money::from("1500000 USD"))
524 );
525 assert_eq!(
526 margin_account.balance_locked(None),
527 Some(Money::from("25000 USD"))
528 );
529 let mut balances_total_expected = HashMap::new();
530 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
531 assert_eq!(margin_account.balances_total(), balances_total_expected);
532 let mut balances_free_expected = HashMap::new();
533 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
534 assert_eq!(margin_account.balances_free(), balances_free_expected);
535 let mut balances_locked_expected = HashMap::new();
536 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
537 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
538 }
539
540 #[rstest]
541 fn test_set_default_leverage(mut margin_account: MarginAccount) {
542 assert_eq!(margin_account.default_leverage, 1.0);
543 margin_account.set_default_leverage(10.0);
544 assert_eq!(margin_account.default_leverage, 10.0);
545 }
546
547 #[rstest]
548 fn test_get_leverage_default_leverage(
549 margin_account: MarginAccount,
550 instrument_id_aud_usd_sim: InstrumentId,
551 ) {
552 assert_eq!(margin_account.get_leverage(&instrument_id_aud_usd_sim), 1.0);
553 }
554
555 #[rstest]
556 fn test_set_leverage(
557 mut margin_account: MarginAccount,
558 instrument_id_aud_usd_sim: InstrumentId,
559 ) {
560 assert_eq!(margin_account.leverages.len(), 0);
561 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
562 assert_eq!(margin_account.leverages.len(), 1);
563 assert_eq!(
564 margin_account.get_leverage(&instrument_id_aud_usd_sim),
565 10.0
566 );
567 }
568
569 #[rstest]
570 fn test_is_unleveraged_with_leverage_returns_false(
571 mut margin_account: MarginAccount,
572 instrument_id_aud_usd_sim: InstrumentId,
573 ) {
574 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
575 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
576 }
577
578 #[rstest]
579 fn test_is_unleveraged_with_no_leverage_returns_true(
580 mut margin_account: MarginAccount,
581 instrument_id_aud_usd_sim: InstrumentId,
582 ) {
583 margin_account.set_leverage(instrument_id_aud_usd_sim, 1.0);
584 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
585 }
586
587 #[rstest]
588 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
589 margin_account: MarginAccount,
590 instrument_id_aud_usd_sim: InstrumentId,
591 ) {
592 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
593 }
594
595 #[rstest]
596 fn test_update_margin_init(
597 mut margin_account: MarginAccount,
598 instrument_id_aud_usd_sim: InstrumentId,
599 ) {
600 assert_eq!(margin_account.margins.len(), 0);
601 let margin = Money::from("10000 USD");
602 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
603 assert_eq!(
604 margin_account.initial_margin(instrument_id_aud_usd_sim),
605 margin
606 );
607 let margins: Vec<Money> = margin_account
608 .margins
609 .values()
610 .map(|margin_balance| margin_balance.initial)
611 .collect();
612 assert_eq!(margins, vec![margin]);
613 }
614
615 #[rstest]
616 fn test_update_margin_maintenance(
617 mut margin_account: MarginAccount,
618 instrument_id_aud_usd_sim: InstrumentId,
619 ) {
620 let margin = Money::from("10000 USD");
621 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
622 assert_eq!(
623 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
624 margin
625 );
626 let margins: Vec<Money> = margin_account
627 .margins
628 .values()
629 .map(|margin_balance| margin_balance.maintenance)
630 .collect();
631 assert_eq!(margins, vec![margin]);
632 }
633
634 #[rstest]
635 fn test_calculate_margin_init_with_leverage(
636 mut margin_account: MarginAccount,
637 audusd_sim: CurrencyPair,
638 ) {
639 margin_account.set_leverage(audusd_sim.id, 50.0);
640 let result = margin_account.calculate_initial_margin(
641 audusd_sim,
642 Quantity::from(100_000),
643 Price::from("0.8000"),
644 None,
645 );
646 assert_eq!(result, Money::from("48.00 USD"));
647 }
648
649 #[rstest]
650 fn test_calculate_margin_init_with_default_leverage(
651 mut margin_account: MarginAccount,
652 audusd_sim: CurrencyPair,
653 ) {
654 margin_account.set_default_leverage(10.0);
655 let result = margin_account.calculate_initial_margin(
656 audusd_sim,
657 Quantity::from(100_000),
658 Price::from("0.8"),
659 None,
660 );
661 assert_eq!(result, Money::from("240.00 USD"));
662 }
663
664 #[rstest]
665 fn test_calculate_margin_init_with_no_leverage_for_inverse(
666 mut margin_account: MarginAccount,
667 xbtusd_bitmex: CryptoPerpetual,
668 ) {
669 let result_use_quote_inverse_true = margin_account.calculate_initial_margin(
670 xbtusd_bitmex,
671 Quantity::from(100_000),
672 Price::from("11493.60"),
673 Some(false),
674 );
675 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
676 let result_use_quote_inverse_false = margin_account.calculate_initial_margin(
677 xbtusd_bitmex,
678 Quantity::from(100_000),
679 Price::from("11493.60"),
680 Some(true),
681 );
682 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
683 }
684
685 #[rstest]
686 fn test_calculate_margin_maintenance_with_no_leverage(
687 mut margin_account: MarginAccount,
688 xbtusd_bitmex: CryptoPerpetual,
689 ) {
690 let result = margin_account.calculate_maintenance_margin(
691 xbtusd_bitmex,
692 Quantity::from(100_000),
693 Price::from("11493.60"),
694 None,
695 );
696 assert_eq!(result, Money::from("0.03045173 BTC"));
697 }
698
699 #[rstest]
700 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
701 mut margin_account: MarginAccount,
702 audusd_sim: CurrencyPair,
703 ) {
704 margin_account.set_default_leverage(50.0);
705 let result = margin_account.calculate_maintenance_margin(
706 audusd_sim,
707 Quantity::from(1_000_000),
708 Price::from("1"),
709 None,
710 );
711 assert_eq!(result, Money::from("600.00 USD"));
712 }
713
714 #[rstest]
715 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
716 mut margin_account: MarginAccount,
717 xbtusd_bitmex: CryptoPerpetual,
718 ) {
719 margin_account.set_default_leverage(10.0);
720 let result = margin_account.calculate_maintenance_margin(
721 xbtusd_bitmex,
722 Quantity::from(100_000),
723 Price::from("100000.00"),
724 None,
725 );
726 assert_eq!(result, Money::from("0.00035000 BTC"));
727 }
728
729 #[rstest]
730 fn test_calculate_pnls_github_issue_2657() {
731 let account_state = margin_account_state();
733 let account = MarginAccount::new(account_state, false);
734
735 let btcusdt = currency_pair_btcusdt();
737 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
738
739 let fill1 = OrderFilled::new(
741 TraderId::from("TRADER-001"),
742 StrategyId::from("S-001"),
743 btcusdt.id,
744 ClientOrderId::from("O-1"),
745 VenueOrderId::from("V-1"),
746 AccountId::from("SIM-001"),
747 TradeId::from("T-1"),
748 OrderSide::Buy,
749 OrderType::Market,
750 Quantity::from("0.001"),
751 Price::from("50000.00"),
752 btcusdt.quote_currency,
753 LiquiditySide::Taker,
754 uuid4(),
755 UnixNanos::from(1_000_000_000),
756 UnixNanos::default(),
757 false,
758 Some(PositionId::from("P-GITHUB-2657")),
759 None,
760 );
761
762 let position = Position::new(&btcusdt_any, fill1);
763
764 let fill2 = OrderFilled::new(
766 TraderId::from("TRADER-001"),
767 StrategyId::from("S-001"),
768 btcusdt.id,
769 ClientOrderId::from("O-2"),
770 VenueOrderId::from("V-2"),
771 AccountId::from("SIM-001"),
772 TradeId::from("T-2"),
773 OrderSide::Sell,
774 OrderType::Market,
775 Quantity::from("0.002"), Price::from("50075.00"),
777 btcusdt.quote_currency,
778 LiquiditySide::Taker,
779 uuid4(),
780 UnixNanos::from(2_000_000_000),
781 UnixNanos::default(),
782 false,
783 Some(PositionId::from("P-GITHUB-2657")),
784 None,
785 );
786
787 let pnls = account
789 .calculate_pnls(btcusdt_any, fill2, Some(position))
790 .unwrap();
791
792 assert_eq!(pnls.len(), 1);
794
795 let expected_pnl = Money::from("0.075 USDT");
798 assert_eq!(pnls[0], expected_pnl);
799 }
800
801 #[rstest]
802 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
803 use nautilus_core::UnixNanos;
804
805 use crate::{
806 enums::{LiquiditySide, OrderSide, OrderType},
807 events::OrderFilled,
808 identifiers::{
809 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
810 stubs::uuid4,
811 },
812 instruments::InstrumentAny,
813 position::Position,
814 types::{Price, Quantity},
815 };
816
817 let account_state = margin_account_state();
819 let account = MarginAccount::new(account_state, false);
820
821 let btcusdt = currency_pair_btcusdt();
823 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
824
825 let fill1 = OrderFilled::new(
827 TraderId::from("TRADER-001"),
828 StrategyId::from("S-001"),
829 btcusdt.id,
830 ClientOrderId::from("O-1"),
831 VenueOrderId::from("V-1"),
832 AccountId::from("SIM-001"),
833 TradeId::from("T-1"),
834 OrderSide::Buy,
835 OrderType::Market,
836 Quantity::from("1.0"),
837 Price::from("50000.00"),
838 btcusdt.quote_currency,
839 LiquiditySide::Taker,
840 uuid4(),
841 UnixNanos::from(1_000_000_000),
842 UnixNanos::default(),
843 false,
844 Some(PositionId::from("P-123456")),
845 None,
846 );
847
848 let position = Position::new(&btcusdt_any, fill1);
849
850 let fill2 = OrderFilled::new(
852 TraderId::from("TRADER-001"),
853 StrategyId::from("S-001"),
854 btcusdt.id,
855 ClientOrderId::from("O-2"),
856 VenueOrderId::from("V-2"),
857 AccountId::from("SIM-001"),
858 TradeId::from("T-2"),
859 OrderSide::Buy, OrderType::Market,
861 Quantity::from("0.5"),
862 Price::from("51000.00"),
863 btcusdt.quote_currency,
864 LiquiditySide::Taker,
865 uuid4(),
866 UnixNanos::from(2_000_000_000),
867 UnixNanos::default(),
868 false,
869 Some(PositionId::from("P-123456")),
870 None,
871 );
872
873 let pnls = account
875 .calculate_pnls(btcusdt_any, fill2, Some(position))
876 .unwrap();
877
878 assert_eq!(pnls.len(), 0);
880 }
881}