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