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 let Some(balance) = margin_balance {
120 let mut new_margin_balance = *balance;
122 new_margin_balance.initial = margin_init;
123 self.margins.insert(instrument_id, new_margin_balance);
124 } else {
125 self.margins.insert(
126 instrument_id,
127 MarginBalance::new(
128 margin_init,
129 Money::new(0.0, margin_init.currency),
130 instrument_id,
131 ),
132 );
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 let Some(balance) = margin_balance {
164 let mut new_margin_balance = *balance;
166 new_margin_balance.maintenance = margin_maintenance;
167 self.margins.insert(instrument_id, new_margin_balance);
168 } else {
169 self.margins.insert(
170 instrument_id,
171 MarginBalance::new(
172 Money::new(0.0, margin_maintenance.currency),
173 margin_maintenance,
174 instrument_id,
175 ),
176 );
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 && pos.quantity.is_positive()
412 && pos.entry != fill.order_side
413 {
414 let pnl_quantity = Quantity::from_raw(
417 fill.last_qty.raw.min(pos.quantity.raw),
418 fill.last_qty.precision,
419 );
420 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
421 pnls.push(pnl);
422 }
423
424 Ok(pnls)
425 }
426
427 fn calculate_commission(
428 &self,
429 instrument: InstrumentAny,
430 last_qty: Quantity,
431 last_px: Price,
432 liquidity_side: LiquiditySide,
433 use_quote_for_inverse: Option<bool>,
434 ) -> anyhow::Result<Money> {
435 self.base_calculate_commission(
436 instrument,
437 last_qty,
438 last_px,
439 liquidity_side,
440 use_quote_for_inverse,
441 )
442 }
443}
444
445impl PartialEq for MarginAccount {
446 fn eq(&self, other: &Self) -> bool {
447 self.id == other.id
448 }
449}
450
451impl Eq for MarginAccount {}
452
453impl Display for MarginAccount {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 write!(
456 f,
457 "MarginAccount(id={}, type={}, base={})",
458 self.id,
459 self.account_type,
460 self.base_currency.map_or_else(
461 || "None".to_string(),
462 |base_currency| format!("{}", base_currency.code)
463 ),
464 )
465 }
466}
467
468impl Hash for MarginAccount {
469 fn hash<H: Hasher>(&self, state: &mut H) {
470 self.id.hash(state);
471 }
472}
473
474#[cfg(test)]
478mod tests {
479 use std::collections::HashMap;
480
481 use nautilus_core::UnixNanos;
482 use rstest::rstest;
483
484 use crate::{
485 accounts::{Account, MarginAccount, stubs::*},
486 enums::{LiquiditySide, OrderSide, OrderType},
487 events::{AccountState, OrderFilled, account::stubs::*},
488 identifiers::{
489 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
490 VenueOrderId,
491 stubs::{uuid4, *},
492 },
493 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
494 position::Position,
495 types::{Currency, Money, Price, Quantity},
496 };
497
498 #[rstest]
499 fn test_display(margin_account: MarginAccount) {
500 assert_eq!(
501 margin_account.to_string(),
502 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
503 );
504 }
505
506 #[rstest]
507 fn test_base_account_properties(
508 margin_account: MarginAccount,
509 margin_account_state: AccountState,
510 ) {
511 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
512 assert_eq!(
513 margin_account.last_event(),
514 Some(margin_account_state.clone())
515 );
516 assert_eq!(margin_account.events(), vec![margin_account_state]);
517 assert_eq!(margin_account.event_count(), 1);
518 assert_eq!(
519 margin_account.balance_total(None),
520 Some(Money::from("1525000 USD"))
521 );
522 assert_eq!(
523 margin_account.balance_free(None),
524 Some(Money::from("1500000 USD"))
525 );
526 assert_eq!(
527 margin_account.balance_locked(None),
528 Some(Money::from("25000 USD"))
529 );
530 let mut balances_total_expected = HashMap::new();
531 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
532 assert_eq!(margin_account.balances_total(), balances_total_expected);
533 let mut balances_free_expected = HashMap::new();
534 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
535 assert_eq!(margin_account.balances_free(), balances_free_expected);
536 let mut balances_locked_expected = HashMap::new();
537 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
538 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
539 }
540
541 #[rstest]
542 fn test_set_default_leverage(mut margin_account: MarginAccount) {
543 assert_eq!(margin_account.default_leverage, 1.0);
544 margin_account.set_default_leverage(10.0);
545 assert_eq!(margin_account.default_leverage, 10.0);
546 }
547
548 #[rstest]
549 fn test_get_leverage_default_leverage(
550 margin_account: MarginAccount,
551 instrument_id_aud_usd_sim: InstrumentId,
552 ) {
553 assert_eq!(margin_account.get_leverage(&instrument_id_aud_usd_sim), 1.0);
554 }
555
556 #[rstest]
557 fn test_set_leverage(
558 mut margin_account: MarginAccount,
559 instrument_id_aud_usd_sim: InstrumentId,
560 ) {
561 assert_eq!(margin_account.leverages.len(), 0);
562 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
563 assert_eq!(margin_account.leverages.len(), 1);
564 assert_eq!(
565 margin_account.get_leverage(&instrument_id_aud_usd_sim),
566 10.0
567 );
568 }
569
570 #[rstest]
571 fn test_is_unleveraged_with_leverage_returns_false(
572 mut margin_account: MarginAccount,
573 instrument_id_aud_usd_sim: InstrumentId,
574 ) {
575 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
576 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
577 }
578
579 #[rstest]
580 fn test_is_unleveraged_with_no_leverage_returns_true(
581 mut margin_account: MarginAccount,
582 instrument_id_aud_usd_sim: InstrumentId,
583 ) {
584 margin_account.set_leverage(instrument_id_aud_usd_sim, 1.0);
585 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
586 }
587
588 #[rstest]
589 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
590 margin_account: MarginAccount,
591 instrument_id_aud_usd_sim: InstrumentId,
592 ) {
593 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
594 }
595
596 #[rstest]
597 fn test_update_margin_init(
598 mut margin_account: MarginAccount,
599 instrument_id_aud_usd_sim: InstrumentId,
600 ) {
601 assert_eq!(margin_account.margins.len(), 0);
602 let margin = Money::from("10000 USD");
603 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
604 assert_eq!(
605 margin_account.initial_margin(instrument_id_aud_usd_sim),
606 margin
607 );
608 let margins: Vec<Money> = margin_account
609 .margins
610 .values()
611 .map(|margin_balance| margin_balance.initial)
612 .collect();
613 assert_eq!(margins, vec![margin]);
614 }
615
616 #[rstest]
617 fn test_update_margin_maintenance(
618 mut margin_account: MarginAccount,
619 instrument_id_aud_usd_sim: InstrumentId,
620 ) {
621 let margin = Money::from("10000 USD");
622 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
623 assert_eq!(
624 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
625 margin
626 );
627 let margins: Vec<Money> = margin_account
628 .margins
629 .values()
630 .map(|margin_balance| margin_balance.maintenance)
631 .collect();
632 assert_eq!(margins, vec![margin]);
633 }
634
635 #[rstest]
636 fn test_calculate_margin_init_with_leverage(
637 mut margin_account: MarginAccount,
638 audusd_sim: CurrencyPair,
639 ) {
640 margin_account.set_leverage(audusd_sim.id, 50.0);
641 let result = margin_account.calculate_initial_margin(
642 audusd_sim,
643 Quantity::from(100_000),
644 Price::from("0.8000"),
645 None,
646 );
647 assert_eq!(result, Money::from("48.00 USD"));
648 }
649
650 #[rstest]
651 fn test_calculate_margin_init_with_default_leverage(
652 mut margin_account: MarginAccount,
653 audusd_sim: CurrencyPair,
654 ) {
655 margin_account.set_default_leverage(10.0);
656 let result = margin_account.calculate_initial_margin(
657 audusd_sim,
658 Quantity::from(100_000),
659 Price::from("0.8"),
660 None,
661 );
662 assert_eq!(result, Money::from("240.00 USD"));
663 }
664
665 #[rstest]
666 fn test_calculate_margin_init_with_no_leverage_for_inverse(
667 mut margin_account: MarginAccount,
668 xbtusd_bitmex: CryptoPerpetual,
669 ) {
670 let result_use_quote_inverse_true = margin_account.calculate_initial_margin(
671 xbtusd_bitmex,
672 Quantity::from(100_000),
673 Price::from("11493.60"),
674 Some(false),
675 );
676 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
677 let result_use_quote_inverse_false = margin_account.calculate_initial_margin(
678 xbtusd_bitmex,
679 Quantity::from(100_000),
680 Price::from("11493.60"),
681 Some(true),
682 );
683 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
684 }
685
686 #[rstest]
687 fn test_calculate_margin_maintenance_with_no_leverage(
688 mut margin_account: MarginAccount,
689 xbtusd_bitmex: CryptoPerpetual,
690 ) {
691 let result = margin_account.calculate_maintenance_margin(
692 xbtusd_bitmex,
693 Quantity::from(100_000),
694 Price::from("11493.60"),
695 None,
696 );
697 assert_eq!(result, Money::from("0.03045173 BTC"));
698 }
699
700 #[rstest]
701 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
702 mut margin_account: MarginAccount,
703 audusd_sim: CurrencyPair,
704 ) {
705 margin_account.set_default_leverage(50.0);
706 let result = margin_account.calculate_maintenance_margin(
707 audusd_sim,
708 Quantity::from(1_000_000),
709 Price::from("1"),
710 None,
711 );
712 assert_eq!(result, Money::from("600.00 USD"));
713 }
714
715 #[rstest]
716 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
717 mut margin_account: MarginAccount,
718 xbtusd_bitmex: CryptoPerpetual,
719 ) {
720 margin_account.set_default_leverage(10.0);
721 let result = margin_account.calculate_maintenance_margin(
722 xbtusd_bitmex,
723 Quantity::from(100_000),
724 Price::from("100000.00"),
725 None,
726 );
727 assert_eq!(result, Money::from("0.00035000 BTC"));
728 }
729
730 #[rstest]
731 fn test_calculate_pnls_github_issue_2657() {
732 let account_state = margin_account_state();
734 let account = MarginAccount::new(account_state, false);
735
736 let btcusdt = currency_pair_btcusdt();
738 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
739
740 let fill1 = OrderFilled::new(
742 TraderId::from("TRADER-001"),
743 StrategyId::from("S-001"),
744 btcusdt.id,
745 ClientOrderId::from("O-1"),
746 VenueOrderId::from("V-1"),
747 AccountId::from("SIM-001"),
748 TradeId::from("T-1"),
749 OrderSide::Buy,
750 OrderType::Market,
751 Quantity::from("0.001"),
752 Price::from("50000.00"),
753 btcusdt.quote_currency,
754 LiquiditySide::Taker,
755 uuid4(),
756 UnixNanos::from(1_000_000_000),
757 UnixNanos::default(),
758 false,
759 Some(PositionId::from("P-GITHUB-2657")),
760 None,
761 );
762
763 let position = Position::new(&btcusdt_any, fill1);
764
765 let fill2 = OrderFilled::new(
767 TraderId::from("TRADER-001"),
768 StrategyId::from("S-001"),
769 btcusdt.id,
770 ClientOrderId::from("O-2"),
771 VenueOrderId::from("V-2"),
772 AccountId::from("SIM-001"),
773 TradeId::from("T-2"),
774 OrderSide::Sell,
775 OrderType::Market,
776 Quantity::from("0.002"), Price::from("50075.00"),
778 btcusdt.quote_currency,
779 LiquiditySide::Taker,
780 uuid4(),
781 UnixNanos::from(2_000_000_000),
782 UnixNanos::default(),
783 false,
784 Some(PositionId::from("P-GITHUB-2657")),
785 None,
786 );
787
788 let pnls = account
790 .calculate_pnls(btcusdt_any, fill2, Some(position))
791 .unwrap();
792
793 assert_eq!(pnls.len(), 1);
795
796 let expected_pnl = Money::from("0.075 USDT");
799 assert_eq!(pnls[0], expected_pnl);
800 }
801
802 #[rstest]
803 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
804 use nautilus_core::UnixNanos;
805
806 use crate::{
807 enums::{LiquiditySide, OrderSide, OrderType},
808 events::OrderFilled,
809 identifiers::{
810 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
811 stubs::uuid4,
812 },
813 instruments::InstrumentAny,
814 position::Position,
815 types::{Price, Quantity},
816 };
817
818 let account_state = margin_account_state();
820 let account = MarginAccount::new(account_state, false);
821
822 let btcusdt = currency_pair_btcusdt();
824 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
825
826 let fill1 = OrderFilled::new(
828 TraderId::from("TRADER-001"),
829 StrategyId::from("S-001"),
830 btcusdt.id,
831 ClientOrderId::from("O-1"),
832 VenueOrderId::from("V-1"),
833 AccountId::from("SIM-001"),
834 TradeId::from("T-1"),
835 OrderSide::Buy,
836 OrderType::Market,
837 Quantity::from("1.0"),
838 Price::from("50000.00"),
839 btcusdt.quote_currency,
840 LiquiditySide::Taker,
841 uuid4(),
842 UnixNanos::from(1_000_000_000),
843 UnixNanos::default(),
844 false,
845 Some(PositionId::from("P-123456")),
846 None,
847 );
848
849 let position = Position::new(&btcusdt_any, fill1);
850
851 let fill2 = OrderFilled::new(
853 TraderId::from("TRADER-001"),
854 StrategyId::from("S-001"),
855 btcusdt.id,
856 ClientOrderId::from("O-2"),
857 VenueOrderId::from("V-2"),
858 AccountId::from("SIM-001"),
859 TradeId::from("T-2"),
860 OrderSide::Buy, OrderType::Market,
862 Quantity::from("0.5"),
863 Price::from("51000.00"),
864 btcusdt.quote_currency,
865 LiquiditySide::Taker,
866 uuid4(),
867 UnixNanos::from(2_000_000_000),
868 UnixNanos::default(),
869 false,
870 Some(PositionId::from("P-123456")),
871 None,
872 );
873
874 let pnls = account
876 .calculate_pnls(btcusdt_any, fill2, Some(position))
877 .unwrap();
878
879 assert_eq!(pnls.len(), 0);
881 }
882}