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