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};
33
34pub struct AccountsManager {
39 clock: Rc<RefCell<dyn Clock>>,
40 cache: Rc<RefCell<Cache>>,
41}
42
43impl Debug for AccountsManager {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct(stringify!(AccountsManager)).finish()
46 }
47}
48
49impl AccountsManager {
50 pub fn new(clock: Rc<RefCell<dyn Clock>>, cache: Rc<RefCell<Cache>>) -> Self {
52 Self { clock, cache }
53 }
54
55 #[must_use]
61 pub fn update_balances(
62 &self,
63 account: AccountAny,
64 instrument: InstrumentAny,
65 fill: OrderFilled,
66 ) -> AccountState {
67 let cache = self.cache.borrow();
68 let position_id = if let Some(position_id) = fill.position_id {
69 position_id
70 } else {
71 let positions_open =
72 cache.positions_open(None, Some(&fill.instrument_id), None, None, None);
73 positions_open
74 .first()
75 .unwrap_or_else(|| panic!("List of Positions is empty"))
76 .id
77 };
78
79 let position = cache.position(&position_id);
80
81 let pnls = account.calculate_pnls(instrument, fill, position.cloned());
82
83 match account.base_currency() {
85 Some(base_currency) => {
86 let pnl = pnls.map_or_else(
87 |_| Money::new(0.0, base_currency),
88 |pnl_list| {
89 pnl_list
90 .first()
91 .copied()
92 .unwrap_or_else(|| Money::new(0.0, base_currency))
93 },
94 );
95
96 self.update_balance_single_currency(account.clone(), &fill, pnl);
97 }
98 None => {
99 if let Ok(mut pnl_list) = pnls {
100 self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list);
101 }
102 }
103 }
104
105 self.generate_account_state(account, fill.ts_event)
107 }
108
109 #[must_use]
114 pub fn update_orders(
115 &self,
116 account: &AccountAny,
117 instrument: InstrumentAny,
118 orders_open: Vec<&OrderAny>,
119 ts_event: UnixNanos,
120 ) -> Option<(AccountAny, AccountState)> {
121 match account.clone() {
122 AccountAny::Cash(cash_account) => self
123 .update_balance_locked(&cash_account, instrument, orders_open, ts_event)
124 .map(|(updated_cash_account, state)| {
125 (AccountAny::Cash(updated_cash_account), state)
126 }),
127 AccountAny::Margin(margin_account) => self
128 .update_margin_init(&margin_account, instrument, orders_open, ts_event)
129 .map(|(updated_margin_account, state)| {
130 (AccountAny::Margin(updated_margin_account), state)
131 }),
132 }
133 }
134
135 #[must_use]
141 pub fn update_positions(
142 &self,
143 account: &MarginAccount,
144 instrument: InstrumentAny,
145 positions: Vec<&Position>,
146 ts_event: UnixNanos,
147 ) -> Option<(MarginAccount, AccountState)> {
148 let mut total_margin_maint = 0.0;
149 let mut base_xrate: Option<f64> = None;
150 let mut currency = instrument.settlement_currency();
151 let mut account = account.clone();
152
153 for position in positions {
154 assert_eq!(
155 position.instrument_id,
156 instrument.id(),
157 "Position not for instrument {}",
158 instrument.id()
159 );
160
161 if !position.is_open() {
162 continue;
163 }
164
165 let margin_maint = match instrument {
166 InstrumentAny::Betting(i) => account
167 .calculate_maintenance_margin(
168 i,
169 position.quantity,
170 instrument.make_price(position.avg_px_open),
171 None,
172 )
173 .ok()?,
174 InstrumentAny::BinaryOption(i) => account
175 .calculate_maintenance_margin(
176 i,
177 position.quantity,
178 instrument.make_price(position.avg_px_open),
179 None,
180 )
181 .ok()?,
182 InstrumentAny::CryptoFuture(i) => account
183 .calculate_maintenance_margin(
184 i,
185 position.quantity,
186 instrument.make_price(position.avg_px_open),
187 None,
188 )
189 .ok()?,
190 InstrumentAny::CryptoOption(i) => account
191 .calculate_maintenance_margin(
192 i,
193 position.quantity,
194 instrument.make_price(position.avg_px_open),
195 None,
196 )
197 .ok()?,
198 InstrumentAny::CryptoPerpetual(i) => account
199 .calculate_maintenance_margin(
200 i,
201 position.quantity,
202 instrument.make_price(position.avg_px_open),
203 None,
204 )
205 .ok()?,
206 InstrumentAny::CurrencyPair(i) => account
207 .calculate_maintenance_margin(
208 i,
209 position.quantity,
210 instrument.make_price(position.avg_px_open),
211 None,
212 )
213 .ok()?,
214 InstrumentAny::Equity(i) => account
215 .calculate_maintenance_margin(
216 i,
217 position.quantity,
218 instrument.make_price(position.avg_px_open),
219 None,
220 )
221 .ok()?,
222 InstrumentAny::FuturesContract(i) => account
223 .calculate_maintenance_margin(
224 i,
225 position.quantity,
226 instrument.make_price(position.avg_px_open),
227 None,
228 )
229 .ok()?,
230 InstrumentAny::FuturesSpread(i) => account
231 .calculate_maintenance_margin(
232 i,
233 position.quantity,
234 instrument.make_price(position.avg_px_open),
235 None,
236 )
237 .ok()?,
238 InstrumentAny::OptionContract(i) => account
239 .calculate_maintenance_margin(
240 i,
241 position.quantity,
242 instrument.make_price(position.avg_px_open),
243 None,
244 )
245 .ok()?,
246 InstrumentAny::OptionSpread(i) => account
247 .calculate_maintenance_margin(
248 i,
249 position.quantity,
250 instrument.make_price(position.avg_px_open),
251 None,
252 )
253 .ok()?,
254 };
255
256 let mut margin_maint = margin_maint.as_f64();
257
258 if let Some(base_currency) = account.base_currency {
259 if base_xrate.is_none() {
260 currency = base_currency;
261 base_xrate = self.calculate_xrate_to_base(
262 AccountAny::Margin(account.clone()),
263 instrument.clone(),
264 position.entry.as_specified(),
265 );
266 }
267
268 if let Some(xrate) = base_xrate {
269 margin_maint *= xrate;
270 } else {
271 log::debug!(
272 "Cannot calculate maintenance (position) margin: insufficient data for {}/{}",
273 instrument.settlement_currency(),
274 base_currency
275 );
276 return None;
277 }
278 }
279
280 total_margin_maint += margin_maint;
281 }
282
283 let margin_maint = Money::new(total_margin_maint, currency);
284 account.update_maintenance_margin(instrument.id(), margin_maint);
285
286 log::info!("{} margin_maint={margin_maint}", instrument.id());
287
288 Some((
290 account.clone(),
291 self.generate_account_state(AccountAny::Margin(account), ts_event),
292 ))
293 }
294
295 fn update_balance_locked(
296 &self,
297 account: &CashAccount,
298 instrument: InstrumentAny,
299 orders_open: Vec<&OrderAny>,
300 ts_event: UnixNanos,
301 ) -> Option<(CashAccount, AccountState)> {
302 let mut account = account.clone();
303
304 if orders_open.is_empty() {
305 account.clear_balance_locked(instrument.id());
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 = *total + locked)
375 .or_insert(locked);
376 }
377
378 if total_locked.is_empty() {
379 account.clear_balance_locked(instrument.id());
380 return Some((
381 account.clone(),
382 self.generate_account_state(AccountAny::Cash(account), ts_event),
383 ));
384 }
385
386 account.clear_balance_locked(instrument.id());
388
389 for (_, balance_locked) in total_locked {
390 account.update_balance_locked(instrument.id(), balance_locked);
391 log::info!("{} balance_locked={balance_locked}", instrument.id());
392 }
393
394 Some((
395 account.clone(),
396 self.generate_account_state(AccountAny::Cash(account), ts_event),
397 ))
398 }
399
400 fn update_margin_init(
401 &self,
402 account: &MarginAccount,
403 instrument: InstrumentAny,
404 orders_open: Vec<&OrderAny>,
405 ts_event: UnixNanos,
406 ) -> Option<(MarginAccount, AccountState)> {
407 let mut total_margin_init = 0.0;
408 let mut base_xrate: Option<f64> = None;
409 let mut currency = instrument.settlement_currency();
410 let mut account = account.clone();
411
412 for order in orders_open {
413 assert_eq!(
414 order.instrument_id(),
415 instrument.id(),
416 "Order not for instrument {}",
417 instrument.id()
418 );
419
420 if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) {
421 continue;
422 }
423
424 if order.is_reduce_only() {
425 continue; }
427
428 let price = if order.price().is_some() {
429 order.price()
430 } else {
431 order.trigger_price()
432 };
433
434 let margin_init = match instrument {
435 InstrumentAny::Betting(i) => account
436 .calculate_initial_margin(i, order.quantity(), price?, None)
437 .ok()?,
438 InstrumentAny::BinaryOption(i) => account
439 .calculate_initial_margin(i, order.quantity(), price?, None)
440 .ok()?,
441 InstrumentAny::CryptoFuture(i) => account
442 .calculate_initial_margin(i, order.quantity(), price?, None)
443 .ok()?,
444 InstrumentAny::CryptoOption(i) => account
445 .calculate_initial_margin(i, order.quantity(), price?, None)
446 .ok()?,
447 InstrumentAny::CryptoPerpetual(i) => account
448 .calculate_initial_margin(i, order.quantity(), price?, None)
449 .ok()?,
450 InstrumentAny::CurrencyPair(i) => account
451 .calculate_initial_margin(i, order.quantity(), price?, None)
452 .ok()?,
453 InstrumentAny::Equity(i) => account
454 .calculate_initial_margin(i, order.quantity(), price?, None)
455 .ok()?,
456 InstrumentAny::FuturesContract(i) => account
457 .calculate_initial_margin(i, order.quantity(), price?, None)
458 .ok()?,
459 InstrumentAny::FuturesSpread(i) => account
460 .calculate_initial_margin(i, order.quantity(), price?, None)
461 .ok()?,
462 InstrumentAny::OptionContract(i) => account
463 .calculate_initial_margin(i, order.quantity(), price?, None)
464 .ok()?,
465 InstrumentAny::OptionSpread(i) => account
466 .calculate_initial_margin(i, order.quantity(), price?, None)
467 .ok()?,
468 };
469
470 let mut margin_init = margin_init.as_f64();
471
472 if let Some(base_currency) = account.base_currency {
473 if base_xrate.is_none() {
474 currency = base_currency;
475 base_xrate = self.calculate_xrate_to_base(
476 AccountAny::Margin(account.clone()),
477 instrument.clone(),
478 order.order_side_specified(),
479 );
480 }
481
482 if let Some(xrate) = base_xrate {
483 margin_init *= xrate;
484 } else {
485 log::debug!(
486 "Cannot calculate initial margin: insufficient data for {}/{}",
487 instrument.settlement_currency(),
488 base_currency
489 );
490 continue;
491 }
492 }
493
494 total_margin_init += margin_init;
495 }
496
497 let money = Money::new(total_margin_init, currency);
498 let margin_init = {
499 account.update_initial_margin(instrument.id(), money);
500 money
501 };
502
503 log::info!("{} margin_init={margin_init}", instrument.id());
504
505 Some((
506 account.clone(),
507 self.generate_account_state(AccountAny::Margin(account), ts_event),
508 ))
509 }
510
511 fn update_balance_single_currency(
512 &self,
513 account: AccountAny,
514 fill: &OrderFilled,
515 mut pnl: Money,
516 ) {
517 let base_currency = if let Some(currency) = account.base_currency() {
518 currency
519 } else {
520 log::error!("Account has no base currency set");
521 return;
522 };
523
524 let mut balances = Vec::new();
525 let mut commission = fill.commission;
526
527 if let Some(ref mut comm) = commission
528 && comm.currency != base_currency
529 {
530 let xrate = self.cache.borrow().get_xrate(
531 fill.instrument_id.venue,
532 comm.currency,
533 base_currency,
534 if fill.order_side == OrderSide::Sell {
535 PriceType::Bid
536 } else {
537 PriceType::Ask
538 },
539 );
540
541 if let Some(xrate) = xrate {
542 *comm = Money::new(comm.as_f64() * xrate, base_currency);
543 } else {
544 log::error!(
545 "Cannot calculate account state: insufficient data for {}/{}",
546 comm.currency,
547 base_currency
548 );
549 return;
550 }
551 }
552
553 if pnl.currency != base_currency {
554 let xrate = self.cache.borrow().get_xrate(
555 fill.instrument_id.venue,
556 pnl.currency,
557 base_currency,
558 if fill.order_side == OrderSide::Sell {
559 PriceType::Bid
560 } else {
561 PriceType::Ask
562 },
563 );
564
565 if let Some(xrate) = xrate {
566 pnl = Money::new(pnl.as_f64() * xrate, base_currency);
567 } else {
568 log::error!(
569 "Cannot calculate account state: insufficient data for {}/{}",
570 pnl.currency,
571 base_currency
572 );
573 return;
574 }
575 }
576
577 if let Some(comm) = commission {
578 pnl = pnl - comm;
579 }
580
581 if pnl.is_zero() {
582 return;
583 }
584
585 let existing_balances = account.balances();
586 let balance = if let Some(b) = existing_balances.get(&pnl.currency) {
587 b
588 } else {
589 log::error!(
590 "Cannot complete transaction: no balance for {}",
591 pnl.currency
592 );
593 return;
594 };
595
596 let new_balance =
597 AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl);
598 balances.push(new_balance);
599
600 match account {
601 AccountAny::Cash(mut cash) => {
602 if let Err(e) = cash.update_balances(&balances) {
603 log::error!("Cannot update cash account balance: {e}");
604 return;
605 }
606 if let Some(comm) = commission {
607 cash.update_commissions(comm);
608 }
609 }
610 AccountAny::Margin(mut margin) => {
611 margin.update_balances(&balances);
612 if let Some(comm) = commission {
613 margin.update_commissions(comm);
614 }
615 }
616 }
617 }
618
619 fn update_balance_multi_currency(
620 &self,
621 account: AccountAny,
622 fill: OrderFilled,
623 pnls: &mut [Money],
624 ) {
625 let mut new_balances = Vec::new();
626 let commission = fill.commission;
627 let mut apply_commission = commission.is_some_and(|c| !c.is_zero());
628
629 for pnl in pnls.iter_mut() {
630 if apply_commission && pnl.currency == commission.unwrap().currency {
631 *pnl = *pnl - commission.unwrap();
632 apply_commission = false;
633 }
634
635 if pnl.is_zero() {
636 continue; }
638
639 let currency = pnl.currency;
640 let balances = account.balances();
641
642 let new_balance = if let Some(balance) = balances.get(¤cy) {
643 let new_total = balance.total.as_f64() + pnl.as_f64();
644 let new_free = balance.free.as_f64() + pnl.as_f64();
645 let total = Money::new(new_total, currency);
646 let free = Money::new(new_free, currency);
647
648 if new_total < 0.0 {
649 log::error!(
650 "AccountBalanceNegative: balance = {}, currency = {}",
651 total.as_decimal(),
652 currency
653 );
654 return;
655 }
656 if new_free < 0.0 {
657 log::error!(
658 "AccountMarginExceeded: balance = {}, margin = {}, currency = {}",
659 total.as_decimal(),
660 balance.locked.as_decimal(),
661 currency
662 );
663 return;
664 }
665
666 AccountBalance::new(total, balance.locked, free)
667 } else {
668 if pnl.as_decimal() < Decimal::ZERO {
669 log::error!(
670 "Cannot complete transaction: no {currency} to deduct a {pnl} realized PnL from"
671 );
672 return;
673 }
674 AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl)
675 };
676
677 new_balances.push(new_balance);
678 }
679
680 if apply_commission {
681 let commission = commission.unwrap();
682 let currency = commission.currency;
683 let balances = account.balances();
684
685 let commission_balance = if let Some(balance) = balances.get(¤cy) {
686 let new_total = balance.total.as_decimal() - commission.as_decimal();
687 let new_free = balance.free.as_decimal() - commission.as_decimal();
688 AccountBalance::new(
689 Money::new(new_total.to_f64().unwrap(), currency),
690 balance.locked,
691 Money::new(new_free.to_f64().unwrap(), currency),
692 )
693 } else {
694 if commission.as_decimal() > Decimal::ZERO {
695 log::error!(
696 "Cannot complete transaction: no {currency} balance to deduct a {commission} commission from"
697 );
698 return;
699 }
700 AccountBalance::new(
701 Money::new(0.0, currency),
702 Money::new(0.0, currency),
703 Money::new(0.0, currency),
704 )
705 };
706 new_balances.push(commission_balance);
707 }
708
709 if new_balances.is_empty() {
710 return;
711 }
712
713 match account {
714 AccountAny::Cash(mut cash) => {
715 if let Err(e) = cash.update_balances(&new_balances) {
716 log::error!("Cannot update cash account balance: {e}");
717 return;
718 }
719 if let Some(commission) = commission {
720 cash.update_commissions(commission);
721 }
722 }
723 AccountAny::Margin(mut margin) => {
724 margin.update_balances(&new_balances);
725 if let Some(commission) = commission {
726 margin.update_commissions(commission);
727 }
728 }
729 }
730 }
731
732 fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState {
733 match account {
734 AccountAny::Cash(cash_account) => AccountState::new(
735 cash_account.id,
736 AccountType::Cash,
737 cash_account.balances.clone().into_values().collect(),
738 vec![],
739 false,
740 UUID4::new(),
741 ts_event,
742 self.clock.borrow().timestamp_ns(),
743 cash_account.base_currency(),
744 ),
745 AccountAny::Margin(margin_account) => AccountState::new(
746 margin_account.id,
747 AccountType::Margin,
748 vec![],
749 margin_account.margins.clone().into_values().collect(),
750 false,
751 UUID4::new(),
752 ts_event,
753 self.clock.borrow().timestamp_ns(),
754 margin_account.base_currency(),
755 ),
756 }
757 }
758
759 fn calculate_xrate_to_base(
760 &self,
761 account: AccountAny,
762 instrument: InstrumentAny,
763 side: OrderSideSpecified,
764 ) -> Option<f64> {
765 match account.base_currency() {
766 None => Some(1.0),
767 Some(base_curr) => self.cache.borrow().get_xrate(
768 instrument.id().venue,
769 instrument.settlement_currency(),
770 base_curr,
771 match side {
772 OrderSideSpecified::Sell => PriceType::Bid,
773 OrderSideSpecified::Buy => PriceType::Ask,
774 },
775 ),
776 }
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use std::{cell::RefCell, rc::Rc};
783
784 use nautilus_common::{cache::Cache, clock::TestClock};
785 use nautilus_model::{
786 accounts::CashAccount,
787 enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType},
788 events::{AccountState, OrderAccepted, OrderEventAny, OrderFilled, OrderSubmitted},
789 identifiers::{AccountId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId},
790 instruments::{InstrumentAny, stubs::audusd_sim},
791 orders::{OrderAny, OrderTestBuilder},
792 position::Position,
793 stubs::TestDefault,
794 types::{AccountBalance, Currency, Money, Price, Quantity},
795 };
796 use rstest::rstest;
797
798 use super::*;
799
800 #[rstest]
801 fn test_update_balance_locked_with_base_currency_multiple_orders() {
802 let usd = Currency::USD();
803 let account_state = AccountState::new(
804 AccountId::new("SIM-001"),
805 AccountType::Cash,
806 vec![AccountBalance::new(
807 Money::new(1_000_000.0, usd),
808 Money::new(0.0, usd),
809 Money::new(1_000_000.0, usd),
810 )],
811 Vec::new(),
812 true,
813 UUID4::new(),
814 UnixNanos::default(),
815 UnixNanos::default(),
816 Some(usd),
817 );
818
819 let account = CashAccount::new(account_state, true, false);
820
821 let clock = Rc::new(RefCell::new(TestClock::new()));
822 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
823 cache
824 .borrow_mut()
825 .add_account(AccountAny::Cash(account.clone()))
826 .unwrap();
827
828 let manager = AccountsManager::new(clock, cache);
829
830 let instrument = audusd_sim();
831
832 let order1 = OrderTestBuilder::new(OrderType::Limit)
833 .instrument_id(instrument.id())
834 .side(OrderSide::Buy)
835 .quantity(Quantity::from("100000"))
836 .price(Price::from("0.75000"))
837 .build();
838
839 let order2 = OrderTestBuilder::new(OrderType::Limit)
840 .instrument_id(instrument.id())
841 .side(OrderSide::Buy)
842 .quantity(Quantity::from("50000"))
843 .price(Price::from("0.74500"))
844 .build();
845
846 let order3 = OrderTestBuilder::new(OrderType::Limit)
847 .instrument_id(instrument.id())
848 .side(OrderSide::Buy)
849 .quantity(Quantity::from("75000"))
850 .price(Price::from("0.74000"))
851 .build();
852
853 let mut order1 = order1;
854 let mut order2 = order2;
855 let mut order3 = order3;
856
857 let submitted1 = OrderSubmitted::new(
858 order1.trader_id(),
859 order1.strategy_id(),
860 order1.instrument_id(),
861 order1.client_order_id(),
862 AccountId::new("SIM-001"),
863 UUID4::new(),
864 UnixNanos::default(),
865 UnixNanos::default(),
866 );
867
868 let accepted1 = OrderAccepted::new(
869 order1.trader_id(),
870 order1.strategy_id(),
871 order1.instrument_id(),
872 order1.client_order_id(),
873 order1.venue_order_id().unwrap_or(VenueOrderId::new("1")),
874 AccountId::new("SIM-001"),
875 UUID4::new(),
876 UnixNanos::default(),
877 UnixNanos::default(),
878 false,
879 );
880
881 order1.apply(OrderEventAny::Submitted(submitted1)).unwrap();
882 order1.apply(OrderEventAny::Accepted(accepted1)).unwrap();
883
884 let submitted2 = OrderSubmitted::new(
885 order2.trader_id(),
886 order2.strategy_id(),
887 order2.instrument_id(),
888 order2.client_order_id(),
889 AccountId::new("SIM-001"),
890 UUID4::new(),
891 UnixNanos::default(),
892 UnixNanos::default(),
893 );
894
895 let accepted2 = OrderAccepted::new(
896 order2.trader_id(),
897 order2.strategy_id(),
898 order2.instrument_id(),
899 order2.client_order_id(),
900 order2.venue_order_id().unwrap_or(VenueOrderId::new("2")),
901 AccountId::new("SIM-001"),
902 UUID4::new(),
903 UnixNanos::default(),
904 UnixNanos::default(),
905 false,
906 );
907
908 order2.apply(OrderEventAny::Submitted(submitted2)).unwrap();
909 order2.apply(OrderEventAny::Accepted(accepted2)).unwrap();
910
911 let submitted3 = OrderSubmitted::new(
912 order3.trader_id(),
913 order3.strategy_id(),
914 order3.instrument_id(),
915 order3.client_order_id(),
916 AccountId::new("SIM-001"),
917 UUID4::new(),
918 UnixNanos::default(),
919 UnixNanos::default(),
920 );
921
922 let accepted3 = OrderAccepted::new(
923 order3.trader_id(),
924 order3.strategy_id(),
925 order3.instrument_id(),
926 order3.client_order_id(),
927 order3.venue_order_id().unwrap_or(VenueOrderId::new("3")),
928 AccountId::new("SIM-001"),
929 UUID4::new(),
930 UnixNanos::default(),
931 UnixNanos::default(),
932 false,
933 );
934
935 order3.apply(OrderEventAny::Submitted(submitted3)).unwrap();
936 order3.apply(OrderEventAny::Accepted(accepted3)).unwrap();
937
938 let orders: Vec<&OrderAny> = vec![&order1, &order2, &order3];
939
940 let result = manager.update_orders(
941 &AccountAny::Cash(account),
942 InstrumentAny::CurrencyPair(instrument),
943 orders,
944 UnixNanos::default(),
945 );
946
947 assert!(result.is_some());
948 let (updated_account, _state) = result.unwrap();
949
950 if let AccountAny::Cash(cash_account) = updated_account {
951 let locked_balance = cash_account.balance_locked(Some(usd));
952
953 let expected_locked = Money::new(167_750.0, usd);
955
956 assert_eq!(locked_balance, Some(expected_locked));
957 let aud = Currency::AUD();
958 assert_eq!(cash_account.balance_locked(Some(aud)), None);
959 } else {
960 panic!("Expected CashAccount");
961 }
962 }
963
964 #[rstest]
965 fn test_update_orders_clears_stale_currency_locks_when_order_sides_change() {
966 let usd = Currency::USD();
967 let aud = Currency::AUD();
968 let account_state = AccountState::new(
969 AccountId::new("SIM-001"),
970 AccountType::Cash,
971 vec![
972 AccountBalance::new(
973 Money::new(1_000_000.0, usd),
974 Money::new(0.0, usd),
975 Money::new(1_000_000.0, usd),
976 ),
977 AccountBalance::new(
978 Money::new(1_000_000.0, aud),
979 Money::new(0.0, aud),
980 Money::new(1_000_000.0, aud),
981 ),
982 ],
983 Vec::new(),
984 true,
985 UUID4::new(),
986 UnixNanos::default(),
987 UnixNanos::default(),
988 None,
989 );
990
991 let account = CashAccount::new(account_state, true, false);
992
993 let clock = Rc::new(RefCell::new(TestClock::new()));
994 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
995 cache
996 .borrow_mut()
997 .add_account(AccountAny::Cash(account.clone()))
998 .unwrap();
999
1000 let manager = AccountsManager::new(clock, cache);
1001 let instrument = audusd_sim();
1002
1003 let mut buy_order = OrderTestBuilder::new(OrderType::Limit)
1004 .instrument_id(instrument.id())
1005 .side(OrderSide::Buy)
1006 .quantity(Quantity::from("100000"))
1007 .price(Price::from("0.80000"))
1008 .build();
1009
1010 let mut sell_order = OrderTestBuilder::new(OrderType::Limit)
1011 .instrument_id(instrument.id())
1012 .side(OrderSide::Sell)
1013 .quantity(Quantity::from("50000"))
1014 .price(Price::from("0.81000"))
1015 .build();
1016
1017 let submitted_buy = OrderSubmitted::new(
1019 buy_order.trader_id(),
1020 buy_order.strategy_id(),
1021 buy_order.instrument_id(),
1022 buy_order.client_order_id(),
1023 AccountId::new("SIM-001"),
1024 UUID4::new(),
1025 UnixNanos::default(),
1026 UnixNanos::default(),
1027 );
1028 let accepted_buy = OrderAccepted::new(
1029 buy_order.trader_id(),
1030 buy_order.strategy_id(),
1031 buy_order.instrument_id(),
1032 buy_order.client_order_id(),
1033 VenueOrderId::new("1"),
1034 AccountId::new("SIM-001"),
1035 UUID4::new(),
1036 UnixNanos::default(),
1037 UnixNanos::default(),
1038 false,
1039 );
1040 buy_order
1041 .apply(OrderEventAny::Submitted(submitted_buy))
1042 .unwrap();
1043 buy_order
1044 .apply(OrderEventAny::Accepted(accepted_buy))
1045 .unwrap();
1046
1047 let submitted_sell = OrderSubmitted::new(
1048 sell_order.trader_id(),
1049 sell_order.strategy_id(),
1050 sell_order.instrument_id(),
1051 sell_order.client_order_id(),
1052 AccountId::new("SIM-001"),
1053 UUID4::new(),
1054 UnixNanos::default(),
1055 UnixNanos::default(),
1056 );
1057 let accepted_sell = OrderAccepted::new(
1058 sell_order.trader_id(),
1059 sell_order.strategy_id(),
1060 sell_order.instrument_id(),
1061 sell_order.client_order_id(),
1062 VenueOrderId::new("2"),
1063 AccountId::new("SIM-001"),
1064 UUID4::new(),
1065 UnixNanos::default(),
1066 UnixNanos::default(),
1067 false,
1068 );
1069 sell_order
1070 .apply(OrderEventAny::Submitted(submitted_sell))
1071 .unwrap();
1072 sell_order
1073 .apply(OrderEventAny::Accepted(accepted_sell))
1074 .unwrap();
1075
1076 let orders_both: Vec<&OrderAny> = vec![&buy_order, &sell_order];
1077 let result = manager.update_orders(
1078 &AccountAny::Cash(account),
1079 InstrumentAny::CurrencyPair(instrument),
1080 orders_both,
1081 UnixNanos::default(),
1082 );
1083
1084 assert!(result.is_some());
1085 let (updated_account, _) = result.unwrap();
1086
1087 if let AccountAny::Cash(cash_account) = &updated_account {
1088 assert_eq!(
1089 cash_account.balance_locked(Some(usd)),
1090 Some(Money::new(80_000.0, usd))
1091 );
1092 assert_eq!(
1093 cash_account.balance_locked(Some(aud)),
1094 Some(Money::new(50_000.0, aud))
1095 );
1096 } else {
1097 panic!("Expected CashAccount");
1098 }
1099
1100 let orders_sell_only: Vec<&OrderAny> = vec![&sell_order];
1102 let result = manager.update_orders(
1103 &updated_account,
1104 InstrumentAny::CurrencyPair(instrument),
1105 orders_sell_only,
1106 UnixNanos::default(),
1107 );
1108
1109 assert!(result.is_some());
1110 let (final_account, _) = result.unwrap();
1111
1112 if let AccountAny::Cash(cash_account) = final_account {
1113 assert_eq!(
1114 cash_account.balance_locked(Some(usd)),
1115 Some(Money::new(0.0, usd))
1116 );
1117 assert_eq!(
1118 cash_account.balance_locked(Some(aud)),
1119 Some(Money::new(50_000.0, aud))
1120 );
1121 } else {
1122 panic!("Expected CashAccount");
1123 }
1124 }
1125
1126 #[rstest]
1127 fn test_cash_account_rejects_negative_balance_when_borrowing_disabled() {
1128 let usd = Currency::USD();
1129 let account_state = AccountState::new(
1130 AccountId::new("SIM-001"),
1131 AccountType::Cash,
1132 vec![AccountBalance::new(
1133 Money::new(1_000.0, usd),
1134 Money::new(0.0, usd),
1135 Money::new(1_000.0, usd),
1136 )],
1137 Vec::new(),
1138 true,
1139 UUID4::new(),
1140 UnixNanos::default(),
1141 UnixNanos::default(),
1142 Some(usd),
1143 );
1144
1145 let mut account = CashAccount::new(account_state, true, false);
1146
1147 let negative_balances = vec![AccountBalance::new(
1148 Money::new(-500.0, usd),
1149 Money::new(0.0, usd),
1150 Money::new(-500.0, usd),
1151 )];
1152
1153 let result = account.update_balances(&negative_balances);
1154
1155 assert!(result.is_err());
1156 let err_msg = result.unwrap_err().to_string();
1157 assert!(err_msg.contains("negative"));
1158 assert!(err_msg.contains("borrowing not allowed"));
1159 }
1160
1161 #[rstest]
1162 fn test_manager_update_balances_skips_update_on_negative_balance_error() {
1163 let usd = Currency::USD();
1164 let account_state = AccountState::new(
1165 AccountId::new("SIM-001"),
1166 AccountType::Cash,
1167 vec![AccountBalance::new(
1168 Money::new(100.0, usd),
1169 Money::new(0.0, usd),
1170 Money::new(100.0, usd),
1171 )],
1172 Vec::new(),
1173 true,
1174 UUID4::new(),
1175 UnixNanos::default(),
1176 UnixNanos::default(),
1177 Some(usd),
1178 );
1179
1180 let account = CashAccount::new(account_state, true, false);
1181 let initial_balance = account.balance_total(Some(usd)).unwrap();
1182
1183 let clock = Rc::new(RefCell::new(TestClock::new()));
1184 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1185 cache
1186 .borrow_mut()
1187 .add_account(AccountAny::Cash(account.clone()))
1188 .unwrap();
1189
1190 let manager = AccountsManager::new(clock, cache.clone());
1191 let instrument = audusd_sim();
1192
1193 let mut order = OrderTestBuilder::new(OrderType::Market)
1194 .instrument_id(instrument.id())
1195 .side(OrderSide::Buy)
1196 .quantity(Quantity::from("100000"))
1197 .build();
1198
1199 let submitted = OrderSubmitted::new(
1200 order.trader_id(),
1201 order.strategy_id(),
1202 order.instrument_id(),
1203 order.client_order_id(),
1204 AccountId::new("SIM-001"),
1205 UUID4::new(),
1206 UnixNanos::default(),
1207 UnixNanos::default(),
1208 );
1209 let accepted = OrderAccepted::new(
1210 order.trader_id(),
1211 order.strategy_id(),
1212 order.instrument_id(),
1213 order.client_order_id(),
1214 VenueOrderId::new("1"),
1215 AccountId::new("SIM-001"),
1216 UUID4::new(),
1217 UnixNanos::default(),
1218 UnixNanos::default(),
1219 false,
1220 );
1221 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1222 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1223
1224 cache
1225 .borrow_mut()
1226 .add_order(order.clone(), None, None, false)
1227 .unwrap();
1228
1229 let fill = OrderFilled::new(
1231 TraderId::test_default(),
1232 StrategyId::test_default(),
1233 instrument.id(),
1234 order.client_order_id(),
1235 VenueOrderId::new("1"),
1236 AccountId::new("SIM-001"),
1237 TradeId::new("1"),
1238 OrderSide::Buy,
1239 order.order_type(),
1240 Quantity::from("100000"),
1241 Price::from("0.80000"),
1242 usd,
1243 LiquiditySide::Taker,
1244 UUID4::new(),
1245 UnixNanos::from(1),
1246 UnixNanos::from(1),
1247 false,
1248 Some(PositionId::new("P-001")),
1249 Some(Money::new(20.0, usd)),
1250 );
1251
1252 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), fill);
1253 cache
1254 .borrow_mut()
1255 .add_position(position, OmsType::Netting)
1256 .unwrap();
1257
1258 let fill2 = OrderFilled::new(
1259 TraderId::test_default(),
1260 StrategyId::test_default(),
1261 instrument.id(),
1262 order.client_order_id(),
1263 VenueOrderId::new("2"),
1264 AccountId::new("SIM-001"),
1265 TradeId::new("2"),
1266 OrderSide::Buy,
1267 order.order_type(),
1268 Quantity::from("100000"),
1269 Price::from("0.80000"),
1270 usd,
1271 LiquiditySide::Taker,
1272 UUID4::new(),
1273 UnixNanos::from(2),
1274 UnixNanos::from(2),
1275 false,
1276 Some(PositionId::new("P-001")),
1277 Some(Money::new(20.0, usd)),
1278 );
1279 let _state = manager.update_balances(
1280 AccountAny::Cash(account),
1281 InstrumentAny::CurrencyPair(instrument),
1282 fill2,
1283 );
1284
1285 let account_after = cache
1286 .borrow()
1287 .account(&AccountId::new("SIM-001"))
1288 .unwrap()
1289 .clone();
1290
1291 if let AccountAny::Cash(cash) = account_after {
1292 assert_eq!(cash.balance_total(Some(usd)), Some(initial_balance));
1293 } else {
1294 panic!("Expected CashAccount");
1295 }
1296 }
1297
1298 #[rstest]
1299 fn test_order_canceled_releases_locked_balance() {
1300 let usd = Currency::USD();
1302 let account_state = AccountState::new(
1303 AccountId::new("SIM-001"),
1304 AccountType::Cash,
1305 vec![AccountBalance::new(
1306 Money::new(100_000.0, usd),
1307 Money::new(0.0, usd),
1308 Money::new(100_000.0, usd),
1309 )],
1310 Vec::new(),
1311 true,
1312 UUID4::new(),
1313 UnixNanos::default(),
1314 UnixNanos::default(),
1315 Some(usd),
1316 );
1317
1318 let account = CashAccount::new(account_state, true, false);
1319
1320 let clock = Rc::new(RefCell::new(TestClock::new()));
1321 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1322 cache
1323 .borrow_mut()
1324 .add_account(AccountAny::Cash(account.clone()))
1325 .unwrap();
1326
1327 let manager = AccountsManager::new(clock, cache);
1328 let instrument = audusd_sim();
1329
1330 let mut order = OrderTestBuilder::new(OrderType::Limit)
1331 .instrument_id(instrument.id())
1332 .side(OrderSide::Buy)
1333 .quantity(Quantity::from("100000"))
1334 .price(Price::from("0.80000"))
1335 .build();
1336
1337 let submitted = OrderSubmitted::new(
1338 order.trader_id(),
1339 order.strategy_id(),
1340 order.instrument_id(),
1341 order.client_order_id(),
1342 AccountId::new("SIM-001"),
1343 UUID4::new(),
1344 UnixNanos::default(),
1345 UnixNanos::default(),
1346 );
1347
1348 let accepted = OrderAccepted::new(
1349 order.trader_id(),
1350 order.strategy_id(),
1351 order.instrument_id(),
1352 order.client_order_id(),
1353 order.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1354 AccountId::new("SIM-001"),
1355 UUID4::new(),
1356 UnixNanos::default(),
1357 UnixNanos::default(),
1358 false,
1359 );
1360
1361 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1362 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1363
1364 let result = manager.update_orders(
1365 &AccountAny::Cash(account),
1366 InstrumentAny::CurrencyPair(instrument),
1367 vec![&order],
1368 UnixNanos::default(),
1369 );
1370
1371 assert!(result.is_some());
1372 let (updated_account, _) = result.unwrap();
1373
1374 if let AccountAny::Cash(ref cash) = updated_account {
1375 assert_eq!(
1377 cash.balance_locked(Some(usd)),
1378 Some(Money::new(80_000.0, usd))
1379 );
1380 assert_eq!(
1381 cash.balance_free(Some(usd)),
1382 Some(Money::new(20_000.0, usd))
1383 );
1384 } else {
1385 panic!("Expected CashAccount");
1386 }
1387
1388 let result = manager.update_orders(
1389 &updated_account,
1390 InstrumentAny::CurrencyPair(instrument),
1391 vec![],
1392 UnixNanos::default(),
1393 );
1394
1395 assert!(result.is_some());
1396 let (final_account, _) = result.unwrap();
1397
1398 if let AccountAny::Cash(cash) = final_account {
1399 assert_eq!(cash.balance_locked(Some(usd)), Some(Money::new(0.0, usd)));
1400 assert_eq!(
1401 cash.balance_free(Some(usd)),
1402 Some(Money::new(100_000.0, usd))
1403 );
1404 assert_eq!(
1405 cash.balance_total(Some(usd)),
1406 Some(Money::new(100_000.0, usd))
1407 );
1408 } else {
1409 panic!("Expected CashAccount");
1410 }
1411 }
1412}