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