1use std::{cell::RefCell, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
21use nautilus_common::{cache::Cache, clock::Clock};
22use nautilus_core::{UUID4, UnixNanos};
23use nautilus_model::{
24 accounts::{Account, AccountAny, CashAccount, MarginAccount},
25 enums::{AccountType, OrderSide, OrderSideSpecified, PriceType},
26 events::{AccountState, OrderFilled},
27 instruments::{Instrument, InstrumentAny},
28 orders::{Order, OrderAny},
29 position::Position,
30 types::{AccountBalance, Currency, Money},
31};
32use rust_decimal::{Decimal, prelude::ToPrimitive};
33pub struct AccountsManager {
38 clock: Rc<RefCell<dyn Clock>>,
39 cache: Rc<RefCell<Cache>>,
40}
41
42impl Debug for AccountsManager {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct(stringify!(AccountsManager)).finish()
45 }
46}
47
48impl AccountsManager {
49 pub fn new(clock: Rc<RefCell<dyn Clock>>, cache: Rc<RefCell<Cache>>) -> Self {
51 Self { clock, cache }
52 }
53
54 #[must_use]
60 pub fn update_balances(
61 &self,
62 account: AccountAny,
63 instrument: InstrumentAny,
64 fill: OrderFilled,
65 ) -> AccountState {
66 let cache = self.cache.borrow();
67 let position_id = if let Some(position_id) = fill.position_id {
68 position_id
69 } else {
70 let positions_open = cache.positions_open(None, Some(&fill.instrument_id), None, None);
71 positions_open
72 .first()
73 .unwrap_or_else(|| panic!("List of Positions is empty"))
74 .id
75 };
76
77 let position = cache.position(&position_id);
78
79 let pnls = account.calculate_pnls(instrument, fill, position.cloned());
80
81 match account.base_currency() {
83 Some(base_currency) => {
84 let pnl = pnls.map_or_else(
85 |_| Money::new(0.0, base_currency),
86 |pnl_list| {
87 pnl_list
88 .first()
89 .copied()
90 .unwrap_or_else(|| Money::new(0.0, base_currency))
91 },
92 );
93
94 self.update_balance_single_currency(account.clone(), &fill, pnl);
95 }
96 None => {
97 if let Ok(mut pnl_list) = pnls {
98 self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list);
99 }
100 }
101 }
102
103 self.generate_account_state(account, fill.ts_event)
105 }
106
107 #[must_use]
112 pub fn update_orders(
113 &self,
114 account: &AccountAny,
115 instrument: InstrumentAny,
116 orders_open: Vec<&OrderAny>,
117 ts_event: UnixNanos,
118 ) -> Option<(AccountAny, AccountState)> {
119 match account.clone() {
120 AccountAny::Cash(cash_account) => self
121 .update_balance_locked(&cash_account, instrument, orders_open, ts_event)
122 .map(|(updated_cash_account, state)| {
123 (AccountAny::Cash(updated_cash_account), state)
124 }),
125 AccountAny::Margin(margin_account) => self
126 .update_margin_init(&margin_account, instrument, orders_open, ts_event)
127 .map(|(updated_margin_account, state)| {
128 (AccountAny::Margin(updated_margin_account), state)
129 }),
130 }
131 }
132
133 #[must_use]
139 pub fn update_positions(
140 &self,
141 account: &MarginAccount,
142 instrument: InstrumentAny,
143 positions: Vec<&Position>,
144 ts_event: UnixNanos,
145 ) -> Option<(MarginAccount, AccountState)> {
146 let mut total_margin_maint = 0.0;
147 let mut base_xrate: Option<f64> = None;
148 let mut currency = instrument.settlement_currency();
149 let mut account = account.clone();
150
151 for position in positions {
152 assert_eq!(
153 position.instrument_id,
154 instrument.id(),
155 "Position not for instrument {}",
156 instrument.id()
157 );
158
159 if !position.is_open() {
160 continue;
161 }
162
163 let margin_maint = match instrument {
164 InstrumentAny::Betting(i) => account
165 .calculate_maintenance_margin(
166 i,
167 position.quantity,
168 instrument.make_price(position.avg_px_open),
169 None,
170 )
171 .ok()?,
172 InstrumentAny::BinaryOption(i) => account
173 .calculate_maintenance_margin(
174 i,
175 position.quantity,
176 instrument.make_price(position.avg_px_open),
177 None,
178 )
179 .ok()?,
180 InstrumentAny::CryptoFuture(i) => account
181 .calculate_maintenance_margin(
182 i,
183 position.quantity,
184 instrument.make_price(position.avg_px_open),
185 None,
186 )
187 .ok()?,
188 InstrumentAny::CryptoOption(i) => account
189 .calculate_maintenance_margin(
190 i,
191 position.quantity,
192 instrument.make_price(position.avg_px_open),
193 None,
194 )
195 .ok()?,
196 InstrumentAny::CryptoPerpetual(i) => account
197 .calculate_maintenance_margin(
198 i,
199 position.quantity,
200 instrument.make_price(position.avg_px_open),
201 None,
202 )
203 .ok()?,
204 InstrumentAny::CurrencyPair(i) => account
205 .calculate_maintenance_margin(
206 i,
207 position.quantity,
208 instrument.make_price(position.avg_px_open),
209 None,
210 )
211 .ok()?,
212 InstrumentAny::Equity(i) => account
213 .calculate_maintenance_margin(
214 i,
215 position.quantity,
216 instrument.make_price(position.avg_px_open),
217 None,
218 )
219 .ok()?,
220 InstrumentAny::FuturesContract(i) => account
221 .calculate_maintenance_margin(
222 i,
223 position.quantity,
224 instrument.make_price(position.avg_px_open),
225 None,
226 )
227 .ok()?,
228 InstrumentAny::FuturesSpread(i) => account
229 .calculate_maintenance_margin(
230 i,
231 position.quantity,
232 instrument.make_price(position.avg_px_open),
233 None,
234 )
235 .ok()?,
236 InstrumentAny::OptionContract(i) => account
237 .calculate_maintenance_margin(
238 i,
239 position.quantity,
240 instrument.make_price(position.avg_px_open),
241 None,
242 )
243 .ok()?,
244 InstrumentAny::OptionSpread(i) => account
245 .calculate_maintenance_margin(
246 i,
247 position.quantity,
248 instrument.make_price(position.avg_px_open),
249 None,
250 )
251 .ok()?,
252 };
253
254 let mut margin_maint = margin_maint.as_f64();
255
256 if let Some(base_currency) = account.base_currency {
257 if base_xrate.is_none() {
258 currency = base_currency;
259 base_xrate = self.calculate_xrate_to_base(
260 AccountAny::Margin(account.clone()),
261 instrument.clone(),
262 position.entry.as_specified(),
263 );
264 }
265
266 if let Some(xrate) = base_xrate {
267 margin_maint *= xrate;
268 } else {
269 log::debug!(
270 "Cannot calculate maintenance (position) margin: insufficient data for {}/{}",
271 instrument.settlement_currency(),
272 base_currency
273 );
274 return None;
275 }
276 }
277
278 total_margin_maint += margin_maint;
279 }
280
281 let margin_maint = Money::new(total_margin_maint, currency);
282 account.update_maintenance_margin(instrument.id(), margin_maint);
283
284 log::info!("{} margin_maint={margin_maint}", instrument.id());
285
286 Some((
288 account.clone(),
289 self.generate_account_state(AccountAny::Margin(account), ts_event),
290 ))
291 }
292
293 fn update_balance_locked(
294 &self,
295 account: &CashAccount,
296 instrument: InstrumentAny,
297 orders_open: Vec<&OrderAny>,
298 ts_event: UnixNanos,
299 ) -> Option<(CashAccount, AccountState)> {
300 let mut account = account.clone();
301 if orders_open.is_empty() {
302 let balance = account.balances.remove(&instrument.quote_currency());
303 if let Some(balance) = balance {
304 account.recalculate_balance(balance.currency);
305 }
306 return Some((
307 account.clone(),
308 self.generate_account_state(AccountAny::Cash(account), ts_event),
309 ));
310 }
311
312 let mut total_locked: AHashMap<Currency, Money> = AHashMap::new();
313 let mut base_xrate: Option<f64> = None;
314
315 let mut currency = instrument.settlement_currency();
316
317 for order in orders_open {
318 assert_eq!(
319 order.instrument_id(),
320 instrument.id(),
321 "Order not for instrument {}",
322 instrument.id()
323 );
324 assert!(order.is_open(), "Order is not open");
325
326 if order.price().is_none() && order.trigger_price().is_none() {
327 continue;
328 }
329
330 if order.is_reduce_only() {
331 continue; }
333
334 let price = if order.price().is_some() {
335 order.price()
336 } else {
337 order.trigger_price()
338 };
339
340 let mut locked = account
341 .calculate_balance_locked(
342 instrument.clone(),
343 order.order_side(),
344 order.quantity(),
345 price?,
346 None,
347 )
348 .unwrap();
349
350 if let Some(base_curr) = account.base_currency() {
351 if base_xrate.is_none() {
352 currency = base_curr;
353 base_xrate = self.calculate_xrate_to_base(
354 AccountAny::Cash(account.clone()),
355 instrument.clone(),
356 order.order_side_specified(),
357 );
358 }
359
360 if let Some(xrate) = base_xrate {
361 locked = Money::new(locked.as_f64() * xrate, currency);
362 } else {
363 log::error!(
364 "Cannot calculate balance locked: insufficient data for {}/{}",
365 instrument.settlement_currency(),
366 base_curr
367 );
368 return None;
369 }
370 }
371
372 total_locked
373 .entry(locked.currency)
374 .and_modify(|total| *total += locked)
375 .or_insert(locked);
376 }
377
378 for (_, balance_locked) in total_locked {
379 if let Some(balance) = account.balances.get_mut(&balance_locked.currency) {
380 balance.locked = balance_locked;
381 let currency = balance.currency;
382 account.recalculate_balance(currency);
383 }
384
385 log::info!("{} balance_locked={balance_locked}", instrument.id());
386 }
387
388 Some((
389 account.clone(),
390 self.generate_account_state(AccountAny::Cash(account), ts_event),
391 ))
392 }
393
394 fn update_margin_init(
395 &self,
396 account: &MarginAccount,
397 instrument: InstrumentAny,
398 orders_open: Vec<&OrderAny>,
399 ts_event: UnixNanos,
400 ) -> Option<(MarginAccount, AccountState)> {
401 let mut total_margin_init = 0.0;
402 let mut base_xrate: Option<f64> = None;
403 let mut currency = instrument.settlement_currency();
404 let mut account = account.clone();
405
406 for order in orders_open {
407 assert_eq!(
408 order.instrument_id(),
409 instrument.id(),
410 "Order not for instrument {}",
411 instrument.id()
412 );
413
414 if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) {
415 continue;
416 }
417
418 if order.is_reduce_only() {
419 continue; }
421
422 let price = if order.price().is_some() {
423 order.price()
424 } else {
425 order.trigger_price()
426 };
427
428 let margin_init = match instrument {
429 InstrumentAny::Betting(i) => account
430 .calculate_initial_margin(i, order.quantity(), price?, None)
431 .ok()?,
432 InstrumentAny::BinaryOption(i) => account
433 .calculate_initial_margin(i, order.quantity(), price?, None)
434 .ok()?,
435 InstrumentAny::CryptoFuture(i) => account
436 .calculate_initial_margin(i, order.quantity(), price?, None)
437 .ok()?,
438 InstrumentAny::CryptoOption(i) => account
439 .calculate_initial_margin(i, order.quantity(), price?, None)
440 .ok()?,
441 InstrumentAny::CryptoPerpetual(i) => account
442 .calculate_initial_margin(i, order.quantity(), price?, None)
443 .ok()?,
444 InstrumentAny::CurrencyPair(i) => account
445 .calculate_initial_margin(i, order.quantity(), price?, None)
446 .ok()?,
447 InstrumentAny::Equity(i) => account
448 .calculate_initial_margin(i, order.quantity(), price?, None)
449 .ok()?,
450 InstrumentAny::FuturesContract(i) => account
451 .calculate_initial_margin(i, order.quantity(), price?, None)
452 .ok()?,
453 InstrumentAny::FuturesSpread(i) => account
454 .calculate_initial_margin(i, order.quantity(), price?, None)
455 .ok()?,
456 InstrumentAny::OptionContract(i) => account
457 .calculate_initial_margin(i, order.quantity(), price?, None)
458 .ok()?,
459 InstrumentAny::OptionSpread(i) => account
460 .calculate_initial_margin(i, order.quantity(), price?, None)
461 .ok()?,
462 };
463
464 let mut margin_init = margin_init.as_f64();
465
466 if let Some(base_currency) = account.base_currency {
467 if base_xrate.is_none() {
468 currency = base_currency;
469 base_xrate = self.calculate_xrate_to_base(
470 AccountAny::Margin(account.clone()),
471 instrument.clone(),
472 order.order_side_specified(),
473 );
474 }
475
476 if let Some(xrate) = base_xrate {
477 margin_init *= xrate;
478 } else {
479 log::debug!(
480 "Cannot calculate initial margin: insufficient data for {}/{}",
481 instrument.settlement_currency(),
482 base_currency
483 );
484 continue;
485 }
486 }
487
488 total_margin_init += margin_init;
489 }
490
491 let money = Money::new(total_margin_init, currency);
492 let margin_init = {
493 account.update_initial_margin(instrument.id(), money);
494 money
495 };
496
497 log::info!("{} margin_init={margin_init}", instrument.id());
498
499 Some((
500 account.clone(),
501 self.generate_account_state(AccountAny::Margin(account), ts_event),
502 ))
503 }
504
505 fn update_balance_single_currency(
506 &self,
507 account: AccountAny,
508 fill: &OrderFilled,
509 mut pnl: Money,
510 ) {
511 let base_currency = if let Some(currency) = account.base_currency() {
512 currency
513 } else {
514 log::error!("Account has no base currency set");
515 return;
516 };
517
518 let mut balances = Vec::new();
519 let mut commission = fill.commission;
520
521 if let Some(ref mut comm) = commission
522 && comm.currency != base_currency
523 {
524 let xrate = self.cache.borrow().get_xrate(
525 fill.instrument_id.venue,
526 comm.currency,
527 base_currency,
528 if fill.order_side == OrderSide::Sell {
529 PriceType::Bid
530 } else {
531 PriceType::Ask
532 },
533 );
534
535 if let Some(xrate) = xrate {
536 *comm = Money::new(comm.as_f64() * xrate, base_currency);
537 } else {
538 log::error!(
539 "Cannot calculate account state: insufficient data for {}/{}",
540 comm.currency,
541 base_currency
542 );
543 return;
544 }
545 }
546
547 if pnl.currency != base_currency {
548 let xrate = self.cache.borrow().get_xrate(
549 fill.instrument_id.venue,
550 pnl.currency,
551 base_currency,
552 if fill.order_side == OrderSide::Sell {
553 PriceType::Bid
554 } else {
555 PriceType::Ask
556 },
557 );
558
559 if let Some(xrate) = xrate {
560 pnl = Money::new(pnl.as_f64() * xrate, base_currency);
561 } else {
562 log::error!(
563 "Cannot calculate account state: insufficient data for {}/{}",
564 pnl.currency,
565 base_currency
566 );
567 return;
568 }
569 }
570
571 if let Some(comm) = commission {
572 pnl -= comm;
573 }
574
575 if pnl.is_zero() {
576 return;
577 }
578
579 let existing_balances = account.balances();
580 let balance = if let Some(b) = existing_balances.get(&pnl.currency) {
581 b
582 } else {
583 log::error!(
584 "Cannot complete transaction: no balance for {}",
585 pnl.currency
586 );
587 return;
588 };
589
590 let new_balance =
591 AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl);
592 balances.push(new_balance);
593
594 match account {
595 AccountAny::Cash(mut cash) => {
596 cash.update_balances(balances);
597 if let Some(comm) = commission {
598 cash.update_commissions(comm);
599 }
600 }
601 AccountAny::Margin(mut margin) => {
602 margin.update_balances(balances);
603 if let Some(comm) = commission {
604 margin.update_commissions(comm);
605 }
606 }
607 }
608 }
609
610 fn update_balance_multi_currency(
611 &self,
612 account: AccountAny,
613 fill: OrderFilled,
614 pnls: &mut [Money],
615 ) {
616 let mut new_balances = Vec::new();
617 let commission = fill.commission;
618 let mut apply_commission = commission.is_some_and(|c| !c.is_zero());
619
620 for pnl in pnls.iter_mut() {
621 if apply_commission && pnl.currency == commission.unwrap().currency {
622 *pnl -= commission.unwrap();
623 apply_commission = false;
624 }
625
626 if pnl.is_zero() {
627 continue; }
629
630 let currency = pnl.currency;
631 let balances = account.balances();
632
633 let new_balance = if let Some(balance) = balances.get(¤cy) {
634 let new_total = balance.total.as_f64() + pnl.as_f64();
635 let new_free = balance.free.as_f64() + pnl.as_f64();
636 let total = Money::new(new_total, currency);
637 let free = Money::new(new_free, currency);
638
639 if new_total < 0.0 {
640 log::error!(
641 "AccountBalanceNegative: balance = {}, currency = {}",
642 total.as_decimal(),
643 currency
644 );
645 return;
646 }
647 if new_free < 0.0 {
648 log::error!(
649 "AccountMarginExceeded: balance = {}, margin = {}, currency = {}",
650 total.as_decimal(),
651 balance.locked.as_decimal(),
652 currency
653 );
654 return;
655 }
656
657 AccountBalance::new(total, balance.locked, free)
658 } else {
659 if pnl.as_decimal() < Decimal::ZERO {
660 log::error!(
661 "Cannot complete transaction: no {currency} to deduct a {pnl} realized PnL from"
662 );
663 return;
664 }
665 AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl)
666 };
667
668 new_balances.push(new_balance);
669 }
670
671 if apply_commission {
672 let commission = commission.unwrap();
673 let currency = commission.currency;
674 let balances = account.balances();
675
676 let commission_balance = if let Some(balance) = balances.get(¤cy) {
677 let new_total = balance.total.as_decimal() - commission.as_decimal();
678 let new_free = balance.free.as_decimal() - commission.as_decimal();
679 AccountBalance::new(
680 Money::new(new_total.to_f64().unwrap(), currency),
681 balance.locked,
682 Money::new(new_free.to_f64().unwrap(), currency),
683 )
684 } else {
685 if commission.as_decimal() > Decimal::ZERO {
686 log::error!(
687 "Cannot complete transaction: no {currency} balance to deduct a {commission} commission from"
688 );
689 return;
690 }
691 AccountBalance::new(
692 Money::new(0.0, currency),
693 Money::new(0.0, currency),
694 Money::new(0.0, currency),
695 )
696 };
697 new_balances.push(commission_balance);
698 }
699
700 if new_balances.is_empty() {
701 return;
702 }
703
704 match account {
705 AccountAny::Cash(mut cash) => {
706 cash.update_balances(new_balances);
707 if let Some(commission) = commission {
708 cash.update_commissions(commission);
709 }
710 }
711 AccountAny::Margin(mut margin) => {
712 margin.update_balances(new_balances);
713 if let Some(commission) = commission {
714 margin.update_commissions(commission);
715 }
716 }
717 }
718 }
719
720 fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState {
721 match account {
722 AccountAny::Cash(cash_account) => AccountState::new(
723 cash_account.id,
724 AccountType::Cash,
725 cash_account.balances.clone().into_values().collect(),
726 vec![],
727 false,
728 UUID4::new(),
729 ts_event,
730 self.clock.borrow().timestamp_ns(),
731 cash_account.base_currency(),
732 ),
733 AccountAny::Margin(margin_account) => AccountState::new(
734 margin_account.id,
735 AccountType::Margin,
736 vec![],
737 margin_account.margins.clone().into_values().collect(),
738 false,
739 UUID4::new(),
740 ts_event,
741 self.clock.borrow().timestamp_ns(),
742 margin_account.base_currency(),
743 ),
744 }
745 }
746
747 fn calculate_xrate_to_base(
748 &self,
749 account: AccountAny,
750 instrument: InstrumentAny,
751 side: OrderSideSpecified,
752 ) -> Option<f64> {
753 match account.base_currency() {
754 None => Some(1.0),
755 Some(base_curr) => self.cache.borrow().get_xrate(
756 instrument.id().venue,
757 instrument.settlement_currency(),
758 base_curr,
759 match side {
760 OrderSideSpecified::Sell => PriceType::Bid,
761 OrderSideSpecified::Buy => PriceType::Ask,
762 },
763 ),
764 }
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use std::{cell::RefCell, rc::Rc};
771
772 use nautilus_common::{cache::Cache, clock::TestClock};
773 use nautilus_model::{
774 accounts::CashAccount,
775 enums::{AccountType, OrderSide, OrderType},
776 events::{AccountState, OrderAccepted, OrderEventAny, OrderSubmitted},
777 identifiers::{AccountId, VenueOrderId},
778 instruments::{InstrumentAny, stubs::audusd_sim},
779 orders::{OrderAny, OrderTestBuilder},
780 types::{AccountBalance, Currency, Money, Price, Quantity},
781 };
782 use rstest::rstest;
783
784 use super::*;
785
786 #[rstest]
787 fn test_update_balance_locked_with_base_currency_multiple_orders() {
788 let usd = Currency::USD();
790 let account_state = AccountState::new(
791 AccountId::new("SIM-001"),
792 AccountType::Cash,
793 vec![AccountBalance::new(
794 Money::new(1_000_000.0, usd),
795 Money::new(0.0, usd),
796 Money::new(1_000_000.0, usd),
797 )],
798 Vec::new(),
799 true,
800 UUID4::new(),
801 UnixNanos::default(),
802 UnixNanos::default(),
803 Some(usd), );
805
806 let account = CashAccount::new(account_state, true, false);
807
808 let clock = Rc::new(RefCell::new(TestClock::new()));
810 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
811 cache
812 .borrow_mut()
813 .add_account(AccountAny::Cash(account.clone()))
814 .unwrap();
815
816 let manager = AccountsManager::new(clock, cache);
817
818 let instrument = audusd_sim();
820
821 let order1 = OrderTestBuilder::new(OrderType::Limit)
823 .instrument_id(instrument.id())
824 .side(OrderSide::Buy)
825 .quantity(Quantity::from("100000"))
826 .price(Price::from("0.75000"))
827 .build();
828
829 let order2 = OrderTestBuilder::new(OrderType::Limit)
830 .instrument_id(instrument.id())
831 .side(OrderSide::Buy)
832 .quantity(Quantity::from("50000"))
833 .price(Price::from("0.74500"))
834 .build();
835
836 let order3 = OrderTestBuilder::new(OrderType::Limit)
837 .instrument_id(instrument.id())
838 .side(OrderSide::Buy)
839 .quantity(Quantity::from("75000"))
840 .price(Price::from("0.74000"))
841 .build();
842
843 let mut order1 = order1;
845 let mut order2 = order2;
846 let mut order3 = order3;
847
848 let submitted1 = OrderSubmitted::new(
849 order1.trader_id(),
850 order1.strategy_id(),
851 order1.instrument_id(),
852 order1.client_order_id(),
853 AccountId::new("SIM-001"),
854 UUID4::new(),
855 UnixNanos::default(),
856 UnixNanos::default(),
857 );
858
859 let accepted1 = OrderAccepted::new(
860 order1.trader_id(),
861 order1.strategy_id(),
862 order1.instrument_id(),
863 order1.client_order_id(),
864 order1.venue_order_id().unwrap_or(VenueOrderId::new("1")),
865 AccountId::new("SIM-001"),
866 UUID4::new(),
867 UnixNanos::default(),
868 UnixNanos::default(),
869 false,
870 );
871
872 order1.apply(OrderEventAny::Submitted(submitted1)).unwrap();
873 order1.apply(OrderEventAny::Accepted(accepted1)).unwrap();
874
875 let submitted2 = OrderSubmitted::new(
876 order2.trader_id(),
877 order2.strategy_id(),
878 order2.instrument_id(),
879 order2.client_order_id(),
880 AccountId::new("SIM-001"),
881 UUID4::new(),
882 UnixNanos::default(),
883 UnixNanos::default(),
884 );
885
886 let accepted2 = OrderAccepted::new(
887 order2.trader_id(),
888 order2.strategy_id(),
889 order2.instrument_id(),
890 order2.client_order_id(),
891 order2.venue_order_id().unwrap_or(VenueOrderId::new("2")),
892 AccountId::new("SIM-001"),
893 UUID4::new(),
894 UnixNanos::default(),
895 UnixNanos::default(),
896 false,
897 );
898
899 order2.apply(OrderEventAny::Submitted(submitted2)).unwrap();
900 order2.apply(OrderEventAny::Accepted(accepted2)).unwrap();
901
902 let submitted3 = OrderSubmitted::new(
903 order3.trader_id(),
904 order3.strategy_id(),
905 order3.instrument_id(),
906 order3.client_order_id(),
907 AccountId::new("SIM-001"),
908 UUID4::new(),
909 UnixNanos::default(),
910 UnixNanos::default(),
911 );
912
913 let accepted3 = OrderAccepted::new(
914 order3.trader_id(),
915 order3.strategy_id(),
916 order3.instrument_id(),
917 order3.client_order_id(),
918 order3.venue_order_id().unwrap_or(VenueOrderId::new("3")),
919 AccountId::new("SIM-001"),
920 UUID4::new(),
921 UnixNanos::default(),
922 UnixNanos::default(),
923 false,
924 );
925
926 order3.apply(OrderEventAny::Submitted(submitted3)).unwrap();
927 order3.apply(OrderEventAny::Accepted(accepted3)).unwrap();
928
929 let orders: Vec<&OrderAny> = vec![&order1, &order2, &order3];
930
931 let result = manager.update_orders(
932 &AccountAny::Cash(account),
933 InstrumentAny::CurrencyPair(instrument),
934 orders,
935 UnixNanos::default(),
936 );
937
938 assert!(result.is_some());
939 let (updated_account, _state) = result.unwrap();
940
941 if let AccountAny::Cash(cash_account) = updated_account {
942 let locked_balance = cash_account.balance_locked(Some(usd));
943
944 let expected_locked = Money::new(167_750.0, usd);
950
951 assert_eq!(locked_balance, Some(expected_locked));
952
953 let aud = Currency::AUD();
955 assert_eq!(cash_account.balance_locked(Some(aud)), None);
956 } else {
957 panic!("Expected CashAccount");
958 }
959 }
960}