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, BettingAccount, CashAccount, MarginAccount},
25 enums::{AccountType, OrderSide, 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 = cache.positions_open(
72 None,
73 Some(&fill.instrument_id),
74 None,
75 Some(&fill.account_id),
76 None,
77 );
78 positions_open
79 .first()
80 .unwrap_or_else(|| panic!("List of Positions is empty"))
81 .id
82 };
83
84 let position = cache.position(&position_id);
85
86 let pnls = account.calculate_pnls(instrument, &fill, position.cloned());
87
88 match account.base_currency() {
90 Some(base_currency) => {
91 let pnl = pnls.map_or_else(
92 |_| Money::new(0.0, base_currency),
93 |pnl_list| {
94 pnl_list
95 .first()
96 .copied()
97 .unwrap_or_else(|| Money::new(0.0, base_currency))
98 },
99 );
100
101 self.update_balance_single_currency(account.clone(), &fill, pnl);
102 }
103 None => {
104 if let Ok(mut pnl_list) = pnls {
105 self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list);
106 }
107 }
108 }
109
110 self.generate_account_state(account, fill.ts_event)
112 }
113
114 #[must_use]
119 pub fn update_orders(
120 &self,
121 account: &AccountAny,
122 instrument: &InstrumentAny,
123 orders_open: Vec<&OrderAny>,
124 ts_event: UnixNanos,
125 ) -> Option<(AccountAny, AccountState)> {
126 match account.clone() {
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 AccountAny::Cash(cash_account) => self
133 .update_balance_locked(&cash_account, instrument, &orders_open, ts_event)
134 .map(|(updated_cash_account, state)| {
135 (AccountAny::Cash(updated_cash_account), state)
136 }),
137 AccountAny::Betting(betting_account) => self
138 .update_balance_locked_betting(&betting_account, instrument, &orders_open, ts_event)
139 .map(|(updated_betting_account, state)| {
140 (AccountAny::Betting(updated_betting_account), state)
141 }),
142 }
143 }
144
145 #[must_use]
151 pub fn update_positions(
152 &self,
153 account: &MarginAccount,
154 instrument: &InstrumentAny,
155 positions: Vec<&Position>,
156 ts_event: UnixNanos,
157 ) -> Option<(MarginAccount, AccountState)> {
158 let mut total_margin_maint = 0.0;
159 let mut base_xrate: Option<f64> = None;
160 let mut currency = instrument.settlement_currency();
161 let mut account = account.clone();
162
163 for position in positions {
164 assert_eq!(
165 position.instrument_id,
166 instrument.id(),
167 "Position not for instrument {}",
168 instrument.id()
169 );
170
171 if !position.is_open() {
172 continue;
173 }
174
175 let margin_maint = match instrument {
176 InstrumentAny::Betting(i) => account
177 .calculate_maintenance_margin(
178 i,
179 position.quantity,
180 instrument.make_price(position.avg_px_open),
181 None,
182 )
183 .ok()?,
184 InstrumentAny::BinaryOption(i) => account
185 .calculate_maintenance_margin(
186 i,
187 position.quantity,
188 instrument.make_price(position.avg_px_open),
189 None,
190 )
191 .ok()?,
192 InstrumentAny::Cfd(i) => account
193 .calculate_maintenance_margin(
194 i,
195 position.quantity,
196 instrument.make_price(position.avg_px_open),
197 None,
198 )
199 .ok()?,
200 InstrumentAny::Commodity(i) => account
201 .calculate_maintenance_margin(
202 i,
203 position.quantity,
204 instrument.make_price(position.avg_px_open),
205 None,
206 )
207 .ok()?,
208 InstrumentAny::CryptoFuture(i) => account
209 .calculate_maintenance_margin(
210 i,
211 position.quantity,
212 instrument.make_price(position.avg_px_open),
213 None,
214 )
215 .ok()?,
216 InstrumentAny::CryptoOption(i) => account
217 .calculate_maintenance_margin(
218 i,
219 position.quantity,
220 instrument.make_price(position.avg_px_open),
221 None,
222 )
223 .ok()?,
224 InstrumentAny::CryptoPerpetual(i) => account
225 .calculate_maintenance_margin(
226 i,
227 position.quantity,
228 instrument.make_price(position.avg_px_open),
229 None,
230 )
231 .ok()?,
232 InstrumentAny::CurrencyPair(i) => account
233 .calculate_maintenance_margin(
234 i,
235 position.quantity,
236 instrument.make_price(position.avg_px_open),
237 None,
238 )
239 .ok()?,
240 InstrumentAny::Equity(i) => account
241 .calculate_maintenance_margin(
242 i,
243 position.quantity,
244 instrument.make_price(position.avg_px_open),
245 None,
246 )
247 .ok()?,
248 InstrumentAny::FuturesContract(i) => account
249 .calculate_maintenance_margin(
250 i,
251 position.quantity,
252 instrument.make_price(position.avg_px_open),
253 None,
254 )
255 .ok()?,
256 InstrumentAny::FuturesSpread(i) => account
257 .calculate_maintenance_margin(
258 i,
259 position.quantity,
260 instrument.make_price(position.avg_px_open),
261 None,
262 )
263 .ok()?,
264 InstrumentAny::IndexInstrument(i) => account
265 .calculate_maintenance_margin(
266 i,
267 position.quantity,
268 instrument.make_price(position.avg_px_open),
269 None,
270 )
271 .ok()?,
272 InstrumentAny::OptionContract(i) => account
273 .calculate_maintenance_margin(
274 i,
275 position.quantity,
276 instrument.make_price(position.avg_px_open),
277 None,
278 )
279 .ok()?,
280 InstrumentAny::OptionSpread(i) => account
281 .calculate_maintenance_margin(
282 i,
283 position.quantity,
284 instrument.make_price(position.avg_px_open),
285 None,
286 )
287 .ok()?,
288 InstrumentAny::PerpetualContract(i) => account
289 .calculate_maintenance_margin(
290 i,
291 position.quantity,
292 instrument.make_price(position.avg_px_open),
293 None,
294 )
295 .ok()?,
296 InstrumentAny::TokenizedAsset(i) => account
297 .calculate_maintenance_margin(
298 i,
299 position.quantity,
300 instrument.make_price(position.avg_px_open),
301 None,
302 )
303 .ok()?,
304 };
305
306 let mut margin_maint = margin_maint.as_f64();
307
308 if let Some(base_currency) = account.base_currency {
309 if base_xrate.is_none() {
310 currency = base_currency;
311 base_xrate = self
312 .calculate_xrate_to_base(&AccountAny::Margin(account.clone()), instrument);
313 }
314
315 if let Some(xrate) = base_xrate {
316 margin_maint *= xrate;
317 } else {
318 log::debug!(
319 "Cannot calculate maintenance (position) margin: insufficient data for {}/{}",
320 instrument.settlement_currency(),
321 base_currency
322 );
323 return None;
324 }
325 }
326
327 total_margin_maint += margin_maint;
328 }
329
330 let margin_maint = Money::new(total_margin_maint, currency);
331 account.update_maintenance_margin(instrument.id(), margin_maint);
332
333 log::info!("{} margin_maint={margin_maint}", instrument.id());
334
335 Some((
337 account.clone(),
338 self.generate_account_state(AccountAny::Margin(account), ts_event),
339 ))
340 }
341
342 fn update_balance_locked(
343 &self,
344 account: &CashAccount,
345 instrument: &InstrumentAny,
346 orders_open: &[&OrderAny],
347 ts_event: UnixNanos,
348 ) -> Option<(CashAccount, AccountState)> {
349 let mut account = account.clone();
350
351 if orders_open.is_empty() {
352 account.clear_balance_locked(instrument.id());
353 return Some((
354 account.clone(),
355 self.generate_account_state(AccountAny::Cash(account), ts_event),
356 ));
357 }
358
359 let mut total_locked: AHashMap<Currency, Money> = AHashMap::new();
360 let mut base_xrate: Option<f64> = None;
361
362 let mut currency = instrument.settlement_currency();
363
364 for order in orders_open {
365 assert_eq!(
366 order.instrument_id(),
367 instrument.id(),
368 "Order not for instrument {}",
369 instrument.id()
370 );
371 assert!(order.is_open(), "Order is not open");
372
373 if order.price().is_none() && order.trigger_price().is_none() {
374 continue;
375 }
376
377 if order.is_reduce_only() {
378 continue; }
380
381 let price = if order.price().is_some() {
382 order.price()
383 } else {
384 order.trigger_price()
385 };
386
387 let mut locked = account
388 .calculate_balance_locked(
389 instrument,
390 order.order_side(),
391 order.quantity(),
392 price?,
393 None,
394 )
395 .unwrap();
396
397 if let Some(base_curr) = account.base_currency() {
398 if base_xrate.is_none() {
399 currency = base_curr;
400 base_xrate = self
401 .calculate_xrate_to_base(&AccountAny::Cash(account.clone()), instrument);
402 }
403
404 if let Some(xrate) = base_xrate {
405 locked = Money::new(locked.as_f64() * xrate, currency);
406 } else {
407 log::error!(
408 "Cannot calculate balance locked: insufficient data for {}/{}",
409 instrument.settlement_currency(),
410 base_curr
411 );
412 return None;
413 }
414 }
415
416 total_locked
417 .entry(locked.currency)
418 .and_modify(|total| *total = *total + locked)
419 .or_insert(locked);
420 }
421
422 if total_locked.is_empty() {
423 account.clear_balance_locked(instrument.id());
424 return Some((
425 account.clone(),
426 self.generate_account_state(AccountAny::Cash(account), ts_event),
427 ));
428 }
429
430 account.clear_balance_locked(instrument.id());
432
433 for (_, balance_locked) in total_locked {
434 account.update_balance_locked(instrument.id(), balance_locked);
435 log::info!("{} balance_locked={balance_locked}", instrument.id());
436 }
437
438 Some((
439 account.clone(),
440 self.generate_account_state(AccountAny::Cash(account), ts_event),
441 ))
442 }
443
444 fn update_margin_init(
445 &self,
446 account: &MarginAccount,
447 instrument: &InstrumentAny,
448 orders_open: Vec<&OrderAny>,
449 ts_event: UnixNanos,
450 ) -> Option<(MarginAccount, AccountState)> {
451 let mut total_margin_init = 0.0;
452 let mut base_xrate: Option<f64> = None;
453 let mut currency = instrument.settlement_currency();
454 let mut account = account.clone();
455
456 for order in orders_open {
457 assert_eq!(
458 order.instrument_id(),
459 instrument.id(),
460 "Order not for instrument {}",
461 instrument.id()
462 );
463
464 if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) {
465 continue;
466 }
467
468 if order.is_reduce_only() {
469 continue; }
471
472 let price = if order.price().is_some() {
473 order.price()
474 } else {
475 order.trigger_price()
476 };
477
478 let margin_init = match instrument {
479 InstrumentAny::Betting(i) => account
480 .calculate_initial_margin(i, order.quantity(), price?, None)
481 .ok()?,
482 InstrumentAny::BinaryOption(i) => account
483 .calculate_initial_margin(i, order.quantity(), price?, None)
484 .ok()?,
485 InstrumentAny::Cfd(i) => account
486 .calculate_initial_margin(i, order.quantity(), price?, None)
487 .ok()?,
488 InstrumentAny::Commodity(i) => account
489 .calculate_initial_margin(i, order.quantity(), price?, None)
490 .ok()?,
491 InstrumentAny::CryptoFuture(i) => account
492 .calculate_initial_margin(i, order.quantity(), price?, None)
493 .ok()?,
494 InstrumentAny::CryptoOption(i) => account
495 .calculate_initial_margin(i, order.quantity(), price?, None)
496 .ok()?,
497 InstrumentAny::CryptoPerpetual(i) => account
498 .calculate_initial_margin(i, order.quantity(), price?, None)
499 .ok()?,
500 InstrumentAny::CurrencyPair(i) => account
501 .calculate_initial_margin(i, order.quantity(), price?, None)
502 .ok()?,
503 InstrumentAny::Equity(i) => account
504 .calculate_initial_margin(i, order.quantity(), price?, None)
505 .ok()?,
506 InstrumentAny::FuturesContract(i) => account
507 .calculate_initial_margin(i, order.quantity(), price?, None)
508 .ok()?,
509 InstrumentAny::FuturesSpread(i) => account
510 .calculate_initial_margin(i, order.quantity(), price?, None)
511 .ok()?,
512 InstrumentAny::IndexInstrument(i) => account
513 .calculate_initial_margin(i, order.quantity(), price?, None)
514 .ok()?,
515 InstrumentAny::OptionContract(i) => account
516 .calculate_initial_margin(i, order.quantity(), price?, None)
517 .ok()?,
518 InstrumentAny::OptionSpread(i) => account
519 .calculate_initial_margin(i, order.quantity(), price?, None)
520 .ok()?,
521 InstrumentAny::PerpetualContract(i) => account
522 .calculate_initial_margin(i, order.quantity(), price?, None)
523 .ok()?,
524 InstrumentAny::TokenizedAsset(i) => account
525 .calculate_initial_margin(i, order.quantity(), price?, None)
526 .ok()?,
527 };
528
529 let mut margin_init = margin_init.as_f64();
530
531 if let Some(base_currency) = account.base_currency {
532 if base_xrate.is_none() {
533 currency = base_currency;
534 base_xrate = self
535 .calculate_xrate_to_base(&AccountAny::Margin(account.clone()), instrument);
536 }
537
538 if let Some(xrate) = base_xrate {
539 margin_init *= xrate;
540 } else {
541 log::debug!(
542 "Cannot calculate initial margin: insufficient data for {}/{}",
543 instrument.settlement_currency(),
544 base_currency
545 );
546 continue;
547 }
548 }
549
550 total_margin_init += margin_init;
551 }
552
553 let money = Money::new(total_margin_init, currency);
554 let margin_init = {
555 account.update_initial_margin(instrument.id(), money);
556 money
557 };
558
559 log::info!("{} margin_init={margin_init}", instrument.id());
560
561 Some((
562 account.clone(),
563 self.generate_account_state(AccountAny::Margin(account), ts_event),
564 ))
565 }
566
567 fn update_balance_locked_betting(
568 &self,
569 account: &BettingAccount,
570 instrument: &InstrumentAny,
571 orders_open: &[&OrderAny],
572 ts_event: UnixNanos,
573 ) -> Option<(BettingAccount, AccountState)> {
574 let mut account = account.clone();
575
576 if orders_open.is_empty() {
577 account.clear_balance_locked(instrument.id());
578 return Some((
579 account.clone(),
580 self.generate_account_state(AccountAny::Betting(account), ts_event),
581 ));
582 }
583
584 let mut total_locked: AHashMap<Currency, Money> = AHashMap::new();
585 let mut base_xrate: Option<f64> = None;
586 let mut currency = instrument.settlement_currency();
587
588 for order in orders_open {
589 assert_eq!(
590 order.instrument_id(),
591 instrument.id(),
592 "Order not for instrument {}",
593 instrument.id()
594 );
595 assert!(order.is_open(), "Order is not open");
596
597 if order.price().is_none() && order.trigger_price().is_none() {
598 continue;
599 }
600
601 if order.is_reduce_only() {
602 continue;
603 }
604
605 let price = if order.price().is_some() {
606 order.price()
607 } else {
608 order.trigger_price()
609 };
610
611 let mut locked = account
612 .calculate_balance_locked(
613 instrument,
614 order.order_side(),
615 order.quantity(),
616 price?,
617 None,
618 )
619 .unwrap();
620
621 if let Some(base_curr) = account.base_currency() {
622 if base_xrate.is_none() {
623 currency = base_curr;
624 base_xrate = self
625 .calculate_xrate_to_base(&AccountAny::Betting(account.clone()), instrument);
626 }
627
628 if let Some(xrate) = base_xrate {
629 locked = Money::new(locked.as_f64() * xrate, currency);
630 } else {
631 log::error!(
632 "Cannot calculate balance locked: insufficient data for {}/{}",
633 instrument.settlement_currency(),
634 base_curr
635 );
636 return None;
637 }
638 }
639
640 total_locked
641 .entry(locked.currency)
642 .and_modify(|total| *total = *total + locked)
643 .or_insert(locked);
644 }
645
646 if total_locked.is_empty() {
647 account.clear_balance_locked(instrument.id());
648 return Some((
649 account.clone(),
650 self.generate_account_state(AccountAny::Betting(account), ts_event),
651 ));
652 }
653
654 account.clear_balance_locked(instrument.id());
655
656 for (_, balance_locked) in total_locked {
657 account.update_balance_locked(instrument.id(), balance_locked);
658 log::info!("{} balance_locked={balance_locked}", instrument.id());
659 }
660
661 Some((
662 account.clone(),
663 self.generate_account_state(AccountAny::Betting(account), ts_event),
664 ))
665 }
666
667 fn update_balance_single_currency(
668 &self,
669 account: AccountAny,
670 fill: &OrderFilled,
671 mut pnl: Money,
672 ) {
673 let base_currency = if let Some(currency) = account.base_currency() {
674 currency
675 } else {
676 log::error!("Account has no base currency set");
677 return;
678 };
679
680 let mut balances = Vec::new();
681 let mut commission = fill.commission;
682
683 if let Some(ref mut comm) = commission
684 && comm.currency != base_currency
685 {
686 let xrate = self.cache.borrow().get_xrate(
687 fill.instrument_id.venue,
688 comm.currency,
689 base_currency,
690 if fill.order_side == OrderSide::Sell {
691 PriceType::Bid
692 } else {
693 PriceType::Ask
694 },
695 );
696
697 if let Some(xrate) = xrate {
698 *comm = Money::new(comm.as_f64() * xrate, base_currency);
699 } else {
700 log::error!(
701 "Cannot calculate account state: insufficient data for {}/{}",
702 comm.currency,
703 base_currency
704 );
705 return;
706 }
707 }
708
709 if pnl.currency != base_currency {
710 let xrate = self.cache.borrow().get_xrate(
711 fill.instrument_id.venue,
712 pnl.currency,
713 base_currency,
714 if fill.order_side == OrderSide::Sell {
715 PriceType::Bid
716 } else {
717 PriceType::Ask
718 },
719 );
720
721 if let Some(xrate) = xrate {
722 pnl = Money::new(pnl.as_f64() * xrate, base_currency);
723 } else {
724 log::error!(
725 "Cannot calculate account state: insufficient data for {}/{}",
726 pnl.currency,
727 base_currency
728 );
729 return;
730 }
731 }
732
733 if let Some(comm) = commission {
734 pnl = pnl - comm;
735 }
736
737 if pnl.is_zero() {
738 return;
739 }
740
741 let existing_balances = account.balances();
742 let balance = if let Some(b) = existing_balances.get(&pnl.currency) {
743 b
744 } else {
745 log::error!(
746 "Cannot complete transaction: no balance for {}",
747 pnl.currency
748 );
749 return;
750 };
751
752 let new_balance =
753 AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl);
754 balances.push(new_balance);
755
756 match account {
757 AccountAny::Margin(mut margin) => {
758 margin.update_balances(&balances);
759
760 if let Some(comm) = commission {
761 margin.update_commissions(comm);
762 }
763 }
764 AccountAny::Cash(mut cash) => {
765 if let Err(e) = cash.update_balances(&balances) {
766 log::error!("Cannot update cash account balance: {e}");
767 return;
768 }
769
770 if let Some(comm) = commission {
771 cash.update_commissions(comm);
772 }
773 }
774 AccountAny::Betting(mut betting) => {
775 if let Err(e) = betting.update_balances(&balances) {
776 log::error!("Cannot update betting account balance: {e}");
777 return;
778 }
779
780 if let Some(comm) = commission {
781 betting.update_commissions(comm);
782 }
783 }
784 }
785 }
786
787 fn update_balance_multi_currency(
788 &self,
789 account: AccountAny,
790 fill: OrderFilled,
791 pnls: &mut [Money],
792 ) {
793 let mut new_balances = Vec::new();
794 let commission = fill.commission;
795 let mut apply_commission = commission.is_some_and(|c| !c.is_zero());
796
797 for pnl in pnls.iter_mut() {
798 if apply_commission && pnl.currency == commission.unwrap().currency {
799 *pnl = *pnl - commission.unwrap();
800 apply_commission = false;
801 }
802
803 if pnl.is_zero() {
804 continue; }
806
807 let currency = pnl.currency;
808 let balances = account.balances();
809
810 let new_balance = if let Some(balance) = balances.get(¤cy) {
811 let new_total = balance.total.as_f64() + pnl.as_f64();
812 let new_free = balance.free.as_f64() + pnl.as_f64();
813 let total = Money::new(new_total, currency);
814 let free = Money::new(new_free, currency);
815
816 if new_total < 0.0 {
817 log::error!(
818 "AccountBalanceNegative: balance = {}, currency = {}",
819 total.as_decimal(),
820 currency
821 );
822 return;
823 }
824
825 if new_free < 0.0 {
826 log::error!(
827 "AccountMarginExceeded: balance = {}, margin = {}, currency = {}",
828 total.as_decimal(),
829 balance.locked.as_decimal(),
830 currency
831 );
832 return;
833 }
834
835 AccountBalance::new(total, balance.locked, free)
836 } else {
837 if pnl.as_decimal() < Decimal::ZERO {
838 log::error!(
839 "Cannot complete transaction: no {currency} to deduct a {pnl} realized PnL from"
840 );
841 return;
842 }
843 AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl)
844 };
845
846 new_balances.push(new_balance);
847 }
848
849 if apply_commission {
850 let commission = commission.unwrap();
851 let currency = commission.currency;
852 let balances = account.balances();
853
854 let commission_balance = if let Some(balance) = balances.get(¤cy) {
855 let new_total = balance.total.as_decimal() - commission.as_decimal();
856 let new_free = balance.free.as_decimal() - commission.as_decimal();
857 AccountBalance::new(
858 Money::new(new_total.to_f64().unwrap(), currency),
859 balance.locked,
860 Money::new(new_free.to_f64().unwrap(), currency),
861 )
862 } else {
863 if commission.as_decimal() > Decimal::ZERO {
864 log::error!(
865 "Cannot complete transaction: no {currency} balance to deduct a {commission} commission from"
866 );
867 return;
868 }
869 AccountBalance::new(
870 Money::new(0.0, currency),
871 Money::new(0.0, currency),
872 Money::new(0.0, currency),
873 )
874 };
875 new_balances.push(commission_balance);
876 }
877
878 if new_balances.is_empty() {
879 return;
880 }
881
882 match account {
883 AccountAny::Margin(mut margin) => {
884 margin.update_balances(&new_balances);
885
886 if let Some(commission) = commission {
887 margin.update_commissions(commission);
888 }
889 }
890 AccountAny::Cash(mut cash) => {
891 if let Err(e) = cash.update_balances(&new_balances) {
892 log::error!("Cannot update cash account balance: {e}");
893 return;
894 }
895
896 if let Some(commission) = commission {
897 cash.update_commissions(commission);
898 }
899 }
900 AccountAny::Betting(mut betting) => {
901 if let Err(e) = betting.update_balances(&new_balances) {
902 log::error!("Cannot update betting account balance: {e}");
903 return;
904 }
905
906 if let Some(commission) = commission {
907 betting.update_commissions(commission);
908 }
909 }
910 }
911 }
912
913 fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState {
914 match account {
915 AccountAny::Margin(margin_account) => {
916 let mut margins: Vec<_> = margin_account.margins.values().copied().collect();
920 margins.extend(margin_account.account_margins.values().copied());
921 AccountState::new(
922 margin_account.id,
923 AccountType::Margin,
924 vec![],
925 margins,
926 false,
927 UUID4::new(),
928 ts_event,
929 self.clock.borrow().timestamp_ns(),
930 margin_account.base_currency(),
931 )
932 }
933 AccountAny::Cash(cash_account) => AccountState::new(
934 cash_account.id,
935 AccountType::Cash,
936 cash_account.balances.clone().into_values().collect(),
937 vec![],
938 false,
939 UUID4::new(),
940 ts_event,
941 self.clock.borrow().timestamp_ns(),
942 cash_account.base_currency(),
943 ),
944 AccountAny::Betting(betting_account) => AccountState::new(
945 betting_account.id,
946 AccountType::Betting,
947 betting_account.balances.clone().into_values().collect(),
948 vec![],
949 false,
950 UUID4::new(),
951 ts_event,
952 self.clock.borrow().timestamp_ns(),
953 betting_account.base_currency(),
954 ),
955 }
956 }
957
958 fn calculate_xrate_to_base(
959 &self,
960 account: &AccountAny,
961 instrument: &InstrumentAny,
962 ) -> Option<f64> {
963 match account.base_currency() {
964 None => Some(1.0),
965 Some(base_curr) => self.cache.borrow().get_xrate(
966 instrument.id().venue,
967 instrument.settlement_currency(),
968 base_curr,
969 PriceType::Mid,
970 ),
971 }
972 }
973}
974
975#[cfg(test)]
976mod tests {
977 use std::{cell::RefCell, rc::Rc};
978
979 use nautilus_common::{cache::Cache, clock::TestClock};
980 use nautilus_model::{
981 accounts::{BettingAccount, CashAccount, MarginAccount},
982 enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType},
983 events::{AccountState, OrderAccepted, OrderEventAny, OrderFilled, OrderSubmitted},
984 identifiers::{
985 AccountId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
986 },
987 instruments::{
988 Instrument, InstrumentAny,
989 stubs::{audusd_sim, betting},
990 },
991 orders::{OrderAny, OrderTestBuilder},
992 position::Position,
993 stubs::TestDefault,
994 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
995 };
996 use rstest::rstest;
997
998 use super::*;
999
1000 #[rstest]
1001 fn test_update_balance_locked_with_base_currency_multiple_orders() {
1002 let usd = Currency::USD();
1003 let account_state = AccountState::new(
1004 AccountId::new("SIM-001"),
1005 AccountType::Cash,
1006 vec![AccountBalance::new(
1007 Money::new(1_000_000.0, usd),
1008 Money::new(0.0, usd),
1009 Money::new(1_000_000.0, usd),
1010 )],
1011 Vec::new(),
1012 true,
1013 UUID4::new(),
1014 UnixNanos::default(),
1015 UnixNanos::default(),
1016 Some(usd),
1017 );
1018
1019 let account = CashAccount::new(account_state, true, false);
1020
1021 let clock = Rc::new(RefCell::new(TestClock::new()));
1022 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1023 cache
1024 .borrow_mut()
1025 .add_account(AccountAny::Cash(account.clone()))
1026 .unwrap();
1027
1028 let manager = AccountsManager::new(clock, cache);
1029
1030 let instrument = audusd_sim();
1031
1032 let order1 = OrderTestBuilder::new(OrderType::Limit)
1033 .instrument_id(instrument.id())
1034 .side(OrderSide::Buy)
1035 .quantity(Quantity::from("100000"))
1036 .price(Price::from("0.75000"))
1037 .build();
1038
1039 let order2 = OrderTestBuilder::new(OrderType::Limit)
1040 .instrument_id(instrument.id())
1041 .side(OrderSide::Buy)
1042 .quantity(Quantity::from("50000"))
1043 .price(Price::from("0.74500"))
1044 .build();
1045
1046 let order3 = OrderTestBuilder::new(OrderType::Limit)
1047 .instrument_id(instrument.id())
1048 .side(OrderSide::Buy)
1049 .quantity(Quantity::from("75000"))
1050 .price(Price::from("0.74000"))
1051 .build();
1052
1053 let mut order1 = order1;
1054 let mut order2 = order2;
1055 let mut order3 = order3;
1056
1057 let submitted1 = OrderSubmitted::new(
1058 order1.trader_id(),
1059 order1.strategy_id(),
1060 order1.instrument_id(),
1061 order1.client_order_id(),
1062 AccountId::new("SIM-001"),
1063 UUID4::new(),
1064 UnixNanos::default(),
1065 UnixNanos::default(),
1066 );
1067
1068 let accepted1 = OrderAccepted::new(
1069 order1.trader_id(),
1070 order1.strategy_id(),
1071 order1.instrument_id(),
1072 order1.client_order_id(),
1073 order1.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1074 AccountId::new("SIM-001"),
1075 UUID4::new(),
1076 UnixNanos::default(),
1077 UnixNanos::default(),
1078 false,
1079 );
1080
1081 order1.apply(OrderEventAny::Submitted(submitted1)).unwrap();
1082 order1.apply(OrderEventAny::Accepted(accepted1)).unwrap();
1083
1084 let submitted2 = OrderSubmitted::new(
1085 order2.trader_id(),
1086 order2.strategy_id(),
1087 order2.instrument_id(),
1088 order2.client_order_id(),
1089 AccountId::new("SIM-001"),
1090 UUID4::new(),
1091 UnixNanos::default(),
1092 UnixNanos::default(),
1093 );
1094
1095 let accepted2 = OrderAccepted::new(
1096 order2.trader_id(),
1097 order2.strategy_id(),
1098 order2.instrument_id(),
1099 order2.client_order_id(),
1100 order2.venue_order_id().unwrap_or(VenueOrderId::new("2")),
1101 AccountId::new("SIM-001"),
1102 UUID4::new(),
1103 UnixNanos::default(),
1104 UnixNanos::default(),
1105 false,
1106 );
1107
1108 order2.apply(OrderEventAny::Submitted(submitted2)).unwrap();
1109 order2.apply(OrderEventAny::Accepted(accepted2)).unwrap();
1110
1111 let submitted3 = OrderSubmitted::new(
1112 order3.trader_id(),
1113 order3.strategy_id(),
1114 order3.instrument_id(),
1115 order3.client_order_id(),
1116 AccountId::new("SIM-001"),
1117 UUID4::new(),
1118 UnixNanos::default(),
1119 UnixNanos::default(),
1120 );
1121
1122 let accepted3 = OrderAccepted::new(
1123 order3.trader_id(),
1124 order3.strategy_id(),
1125 order3.instrument_id(),
1126 order3.client_order_id(),
1127 order3.venue_order_id().unwrap_or(VenueOrderId::new("3")),
1128 AccountId::new("SIM-001"),
1129 UUID4::new(),
1130 UnixNanos::default(),
1131 UnixNanos::default(),
1132 false,
1133 );
1134
1135 order3.apply(OrderEventAny::Submitted(submitted3)).unwrap();
1136 order3.apply(OrderEventAny::Accepted(accepted3)).unwrap();
1137
1138 let orders: Vec<&OrderAny> = vec![&order1, &order2, &order3];
1139
1140 let result = manager.update_orders(
1141 &AccountAny::Cash(account),
1142 &InstrumentAny::CurrencyPair(instrument),
1143 orders,
1144 UnixNanos::default(),
1145 );
1146
1147 assert!(result.is_some());
1148 let (updated_account, _state) = result.unwrap();
1149
1150 if let AccountAny::Cash(cash_account) = updated_account {
1151 let locked_balance = cash_account.balance_locked(Some(usd));
1152
1153 let expected_locked = Money::new(167_750.0, usd);
1155
1156 assert_eq!(locked_balance, Some(expected_locked));
1157 let aud = Currency::AUD();
1158 assert_eq!(cash_account.balance_locked(Some(aud)), None);
1159 } else {
1160 panic!("Expected CashAccount");
1161 }
1162 }
1163
1164 #[rstest]
1165 fn test_update_orders_betting_account_uses_liability_for_locked_balance() {
1166 let gbp = Currency::GBP();
1167 let account_state = AccountState::new(
1168 AccountId::new("BETTING-001"),
1169 AccountType::Betting,
1170 vec![AccountBalance::new(
1171 Money::new(1_000.0, gbp),
1172 Money::new(0.0, gbp),
1173 Money::new(1_000.0, gbp),
1174 )],
1175 Vec::new(),
1176 true,
1177 UUID4::new(),
1178 UnixNanos::default(),
1179 UnixNanos::default(),
1180 Some(gbp),
1181 );
1182
1183 let account = BettingAccount::new(account_state, true);
1184
1185 let clock = Rc::new(RefCell::new(TestClock::new()));
1186 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1187 cache
1188 .borrow_mut()
1189 .add_account(AccountAny::Betting(account.clone()))
1190 .unwrap();
1191
1192 let manager = AccountsManager::new(clock, cache);
1193 let instrument = betting();
1194
1195 let mut back_order = OrderTestBuilder::new(OrderType::Limit)
1196 .instrument_id(instrument.id())
1197 .side(OrderSide::Buy)
1198 .quantity(Quantity::from("10"))
1199 .price(Price::from("1.25"))
1200 .build();
1201
1202 let mut lay_order = OrderTestBuilder::new(OrderType::Limit)
1203 .instrument_id(instrument.id())
1204 .side(OrderSide::Sell)
1205 .quantity(Quantity::from("12"))
1206 .price(Price::from("3.00"))
1207 .build();
1208
1209 let submitted_back = OrderSubmitted::new(
1210 back_order.trader_id(),
1211 back_order.strategy_id(),
1212 back_order.instrument_id(),
1213 back_order.client_order_id(),
1214 AccountId::new("BETTING-001"),
1215 UUID4::new(),
1216 UnixNanos::default(),
1217 UnixNanos::default(),
1218 );
1219 let accepted_back = OrderAccepted::new(
1220 back_order.trader_id(),
1221 back_order.strategy_id(),
1222 back_order.instrument_id(),
1223 back_order.client_order_id(),
1224 VenueOrderId::new("B1"),
1225 AccountId::new("BETTING-001"),
1226 UUID4::new(),
1227 UnixNanos::default(),
1228 UnixNanos::default(),
1229 false,
1230 );
1231 back_order
1232 .apply(OrderEventAny::Submitted(submitted_back))
1233 .unwrap();
1234 back_order
1235 .apply(OrderEventAny::Accepted(accepted_back))
1236 .unwrap();
1237
1238 let submitted_lay = OrderSubmitted::new(
1239 lay_order.trader_id(),
1240 lay_order.strategy_id(),
1241 lay_order.instrument_id(),
1242 lay_order.client_order_id(),
1243 AccountId::new("BETTING-001"),
1244 UUID4::new(),
1245 UnixNanos::default(),
1246 UnixNanos::default(),
1247 );
1248 let accepted_lay = OrderAccepted::new(
1249 lay_order.trader_id(),
1250 lay_order.strategy_id(),
1251 lay_order.instrument_id(),
1252 lay_order.client_order_id(),
1253 VenueOrderId::new("L1"),
1254 AccountId::new("BETTING-001"),
1255 UUID4::new(),
1256 UnixNanos::default(),
1257 UnixNanos::default(),
1258 false,
1259 );
1260 lay_order
1261 .apply(OrderEventAny::Submitted(submitted_lay))
1262 .unwrap();
1263 lay_order
1264 .apply(OrderEventAny::Accepted(accepted_lay))
1265 .unwrap();
1266
1267 let orders: Vec<&OrderAny> = vec![&back_order, &lay_order];
1268 let result = manager.update_orders(
1269 &AccountAny::Betting(account),
1270 &InstrumentAny::Betting(instrument),
1271 orders,
1272 UnixNanos::default(),
1273 );
1274
1275 assert!(result.is_some());
1276 let (updated_account, state) = result.unwrap();
1277
1278 if let AccountAny::Betting(betting_account) = updated_account {
1279 assert_eq!(
1280 betting_account.balance_locked(Some(gbp)),
1281 Some(Money::new(14.5, gbp))
1282 );
1283 assert_eq!(
1284 betting_account.balance_free(Some(gbp)),
1285 Some(Money::new(985.5, gbp))
1286 );
1287 assert_eq!(state.account_type, AccountType::Betting);
1288 } else {
1289 panic!("Expected BettingAccount");
1290 }
1291 }
1292
1293 #[rstest]
1294 fn test_betting_order_canceled_releases_locked_balance() {
1295 let gbp = Currency::GBP();
1296 let account_state = AccountState::new(
1297 AccountId::new("BETFAIR-001"),
1298 AccountType::Betting,
1299 vec![AccountBalance::new(
1300 Money::new(1_000.0, gbp),
1301 Money::new(0.0, gbp),
1302 Money::new(1_000.0, gbp),
1303 )],
1304 Vec::new(),
1305 true,
1306 UUID4::new(),
1307 UnixNanos::default(),
1308 UnixNanos::default(),
1309 Some(gbp),
1310 );
1311
1312 let account = BettingAccount::new(account_state, true);
1313
1314 let clock = Rc::new(RefCell::new(TestClock::new()));
1315 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1316 cache
1317 .borrow_mut()
1318 .add_account(AccountAny::Betting(account.clone()))
1319 .unwrap();
1320
1321 let manager = AccountsManager::new(clock, cache);
1322 let instrument = betting();
1323
1324 let mut order = OrderTestBuilder::new(OrderType::Limit)
1325 .instrument_id(instrument.id())
1326 .side(OrderSide::Buy)
1327 .quantity(Quantity::from("10"))
1328 .price(Price::from("5.0"))
1329 .build();
1330
1331 let submitted = OrderSubmitted::new(
1332 order.trader_id(),
1333 order.strategy_id(),
1334 order.instrument_id(),
1335 order.client_order_id(),
1336 AccountId::new("BETFAIR-001"),
1337 UUID4::new(),
1338 UnixNanos::default(),
1339 UnixNanos::default(),
1340 );
1341 let accepted = OrderAccepted::new(
1342 order.trader_id(),
1343 order.strategy_id(),
1344 order.instrument_id(),
1345 order.client_order_id(),
1346 VenueOrderId::new("B2"),
1347 AccountId::new("BETFAIR-001"),
1348 UUID4::new(),
1349 UnixNanos::default(),
1350 UnixNanos::default(),
1351 false,
1352 );
1353
1354 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1355 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1356
1357 let result = manager.update_orders(
1358 &AccountAny::Betting(account),
1359 &InstrumentAny::Betting(instrument.clone()),
1360 vec![&order],
1361 UnixNanos::default(),
1362 );
1363
1364 assert!(result.is_some());
1365 let (updated_account, _) = result.unwrap();
1366
1367 if let AccountAny::Betting(ref betting_account) = updated_account {
1368 assert_eq!(
1369 betting_account.balance_locked(Some(gbp)),
1370 Some(Money::new(40.0, gbp))
1371 );
1372 assert_eq!(
1373 betting_account.balance_free(Some(gbp)),
1374 Some(Money::new(960.0, gbp))
1375 );
1376 } else {
1377 panic!("Expected BettingAccount");
1378 }
1379
1380 let result = manager.update_orders(
1381 &updated_account,
1382 &InstrumentAny::Betting(instrument),
1383 vec![],
1384 UnixNanos::default(),
1385 );
1386
1387 assert!(result.is_some());
1388 let (final_account, _) = result.unwrap();
1389
1390 if let AccountAny::Betting(betting_account) = final_account {
1391 assert_eq!(
1392 betting_account.balance_locked(Some(gbp)),
1393 Some(Money::new(0.0, gbp))
1394 );
1395 assert_eq!(
1396 betting_account.balance_free(Some(gbp)),
1397 Some(Money::new(1_000.0, gbp))
1398 );
1399 assert_eq!(
1400 betting_account.balance_total(Some(gbp)),
1401 Some(Money::new(1_000.0, gbp))
1402 );
1403 } else {
1404 panic!("Expected BettingAccount");
1405 }
1406 }
1407
1408 #[rstest]
1409 fn test_update_orders_clears_stale_currency_locks_when_order_sides_change() {
1410 let usd = Currency::USD();
1411 let aud = Currency::AUD();
1412 let account_state = AccountState::new(
1413 AccountId::new("SIM-001"),
1414 AccountType::Cash,
1415 vec![
1416 AccountBalance::new(
1417 Money::new(1_000_000.0, usd),
1418 Money::new(0.0, usd),
1419 Money::new(1_000_000.0, usd),
1420 ),
1421 AccountBalance::new(
1422 Money::new(1_000_000.0, aud),
1423 Money::new(0.0, aud),
1424 Money::new(1_000_000.0, aud),
1425 ),
1426 ],
1427 Vec::new(),
1428 true,
1429 UUID4::new(),
1430 UnixNanos::default(),
1431 UnixNanos::default(),
1432 None,
1433 );
1434
1435 let account = CashAccount::new(account_state, true, false);
1436
1437 let clock = Rc::new(RefCell::new(TestClock::new()));
1438 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1439 cache
1440 .borrow_mut()
1441 .add_account(AccountAny::Cash(account.clone()))
1442 .unwrap();
1443
1444 let manager = AccountsManager::new(clock, cache);
1445 let instrument = audusd_sim();
1446
1447 let mut buy_order = OrderTestBuilder::new(OrderType::Limit)
1448 .instrument_id(instrument.id())
1449 .side(OrderSide::Buy)
1450 .quantity(Quantity::from("100000"))
1451 .price(Price::from("0.80000"))
1452 .build();
1453
1454 let mut sell_order = OrderTestBuilder::new(OrderType::Limit)
1455 .instrument_id(instrument.id())
1456 .side(OrderSide::Sell)
1457 .quantity(Quantity::from("50000"))
1458 .price(Price::from("0.81000"))
1459 .build();
1460
1461 let submitted_buy = OrderSubmitted::new(
1463 buy_order.trader_id(),
1464 buy_order.strategy_id(),
1465 buy_order.instrument_id(),
1466 buy_order.client_order_id(),
1467 AccountId::new("SIM-001"),
1468 UUID4::new(),
1469 UnixNanos::default(),
1470 UnixNanos::default(),
1471 );
1472 let accepted_buy = OrderAccepted::new(
1473 buy_order.trader_id(),
1474 buy_order.strategy_id(),
1475 buy_order.instrument_id(),
1476 buy_order.client_order_id(),
1477 VenueOrderId::new("1"),
1478 AccountId::new("SIM-001"),
1479 UUID4::new(),
1480 UnixNanos::default(),
1481 UnixNanos::default(),
1482 false,
1483 );
1484 buy_order
1485 .apply(OrderEventAny::Submitted(submitted_buy))
1486 .unwrap();
1487 buy_order
1488 .apply(OrderEventAny::Accepted(accepted_buy))
1489 .unwrap();
1490
1491 let submitted_sell = OrderSubmitted::new(
1492 sell_order.trader_id(),
1493 sell_order.strategy_id(),
1494 sell_order.instrument_id(),
1495 sell_order.client_order_id(),
1496 AccountId::new("SIM-001"),
1497 UUID4::new(),
1498 UnixNanos::default(),
1499 UnixNanos::default(),
1500 );
1501 let accepted_sell = OrderAccepted::new(
1502 sell_order.trader_id(),
1503 sell_order.strategy_id(),
1504 sell_order.instrument_id(),
1505 sell_order.client_order_id(),
1506 VenueOrderId::new("2"),
1507 AccountId::new("SIM-001"),
1508 UUID4::new(),
1509 UnixNanos::default(),
1510 UnixNanos::default(),
1511 false,
1512 );
1513 sell_order
1514 .apply(OrderEventAny::Submitted(submitted_sell))
1515 .unwrap();
1516 sell_order
1517 .apply(OrderEventAny::Accepted(accepted_sell))
1518 .unwrap();
1519
1520 let orders_both: Vec<&OrderAny> = vec![&buy_order, &sell_order];
1521 let result = manager.update_orders(
1522 &AccountAny::Cash(account),
1523 &InstrumentAny::CurrencyPair(instrument.clone()),
1524 orders_both,
1525 UnixNanos::default(),
1526 );
1527
1528 assert!(result.is_some());
1529 let (updated_account, _) = result.unwrap();
1530
1531 if let AccountAny::Cash(cash_account) = &updated_account {
1532 assert_eq!(
1533 cash_account.balance_locked(Some(usd)),
1534 Some(Money::new(80_000.0, usd))
1535 );
1536 assert_eq!(
1537 cash_account.balance_locked(Some(aud)),
1538 Some(Money::new(50_000.0, aud))
1539 );
1540 } else {
1541 panic!("Expected CashAccount");
1542 }
1543
1544 let orders_sell_only: Vec<&OrderAny> = vec![&sell_order];
1546 let result = manager.update_orders(
1547 &updated_account,
1548 &InstrumentAny::CurrencyPair(instrument),
1549 orders_sell_only,
1550 UnixNanos::default(),
1551 );
1552
1553 assert!(result.is_some());
1554 let (final_account, _) = result.unwrap();
1555
1556 if let AccountAny::Cash(cash_account) = final_account {
1557 assert_eq!(
1558 cash_account.balance_locked(Some(usd)),
1559 Some(Money::new(0.0, usd))
1560 );
1561 assert_eq!(
1562 cash_account.balance_locked(Some(aud)),
1563 Some(Money::new(50_000.0, aud))
1564 );
1565 } else {
1566 panic!("Expected CashAccount");
1567 }
1568 }
1569
1570 #[rstest]
1571 fn test_cash_account_rejects_negative_balance_when_borrowing_disabled() {
1572 let usd = Currency::USD();
1573 let account_state = AccountState::new(
1574 AccountId::new("SIM-001"),
1575 AccountType::Cash,
1576 vec![AccountBalance::new(
1577 Money::new(1_000.0, usd),
1578 Money::new(0.0, usd),
1579 Money::new(1_000.0, usd),
1580 )],
1581 Vec::new(),
1582 true,
1583 UUID4::new(),
1584 UnixNanos::default(),
1585 UnixNanos::default(),
1586 Some(usd),
1587 );
1588
1589 let mut account = CashAccount::new(account_state, true, false);
1590
1591 let negative_balances = vec![AccountBalance::new(
1592 Money::new(-500.0, usd),
1593 Money::new(0.0, usd),
1594 Money::new(-500.0, usd),
1595 )];
1596
1597 let result = account.update_balances(&negative_balances);
1598
1599 assert!(result.is_err());
1600 let err_msg = result.unwrap_err().to_string();
1601 assert!(err_msg.contains("negative"));
1602 assert!(err_msg.contains("borrowing not allowed"));
1603 }
1604
1605 #[rstest]
1606 fn test_manager_update_balances_skips_update_on_negative_balance_error() {
1607 let usd = Currency::USD();
1608 let account_state = AccountState::new(
1609 AccountId::new("SIM-001"),
1610 AccountType::Cash,
1611 vec![AccountBalance::new(
1612 Money::new(100.0, usd),
1613 Money::new(0.0, usd),
1614 Money::new(100.0, usd),
1615 )],
1616 Vec::new(),
1617 true,
1618 UUID4::new(),
1619 UnixNanos::default(),
1620 UnixNanos::default(),
1621 Some(usd),
1622 );
1623
1624 let account = CashAccount::new(account_state, true, false);
1625 let initial_balance = account.balance_total(Some(usd)).unwrap();
1626
1627 let clock = Rc::new(RefCell::new(TestClock::new()));
1628 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1629 cache
1630 .borrow_mut()
1631 .add_account(AccountAny::Cash(account.clone()))
1632 .unwrap();
1633
1634 let manager = AccountsManager::new(clock, cache.clone());
1635 let instrument = audusd_sim();
1636
1637 let mut order = OrderTestBuilder::new(OrderType::Market)
1638 .instrument_id(instrument.id())
1639 .side(OrderSide::Buy)
1640 .quantity(Quantity::from("100000"))
1641 .build();
1642
1643 let submitted = OrderSubmitted::new(
1644 order.trader_id(),
1645 order.strategy_id(),
1646 order.instrument_id(),
1647 order.client_order_id(),
1648 AccountId::new("SIM-001"),
1649 UUID4::new(),
1650 UnixNanos::default(),
1651 UnixNanos::default(),
1652 );
1653 let accepted = OrderAccepted::new(
1654 order.trader_id(),
1655 order.strategy_id(),
1656 order.instrument_id(),
1657 order.client_order_id(),
1658 VenueOrderId::new("1"),
1659 AccountId::new("SIM-001"),
1660 UUID4::new(),
1661 UnixNanos::default(),
1662 UnixNanos::default(),
1663 false,
1664 );
1665 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1666 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1667
1668 cache
1669 .borrow_mut()
1670 .add_order(order.clone(), None, None, false)
1671 .unwrap();
1672
1673 let fill = OrderFilled::new(
1675 TraderId::test_default(),
1676 StrategyId::test_default(),
1677 instrument.id(),
1678 order.client_order_id(),
1679 VenueOrderId::new("1"),
1680 AccountId::new("SIM-001"),
1681 TradeId::new("1"),
1682 OrderSide::Buy,
1683 order.order_type(),
1684 Quantity::from("100000"),
1685 Price::from("0.80000"),
1686 usd,
1687 LiquiditySide::Taker,
1688 UUID4::new(),
1689 UnixNanos::from(1),
1690 UnixNanos::from(1),
1691 false,
1692 Some(PositionId::new("P-001")),
1693 Some(Money::new(20.0, usd)),
1694 );
1695
1696 let position = Position::new(&InstrumentAny::CurrencyPair(instrument.clone()), fill);
1697 cache
1698 .borrow_mut()
1699 .add_position(&position, OmsType::Netting)
1700 .unwrap();
1701
1702 let fill2 = OrderFilled::new(
1703 TraderId::test_default(),
1704 StrategyId::test_default(),
1705 instrument.id(),
1706 order.client_order_id(),
1707 VenueOrderId::new("2"),
1708 AccountId::new("SIM-001"),
1709 TradeId::new("2"),
1710 OrderSide::Buy,
1711 order.order_type(),
1712 Quantity::from("100000"),
1713 Price::from("0.80000"),
1714 usd,
1715 LiquiditySide::Taker,
1716 UUID4::new(),
1717 UnixNanos::from(2),
1718 UnixNanos::from(2),
1719 false,
1720 Some(PositionId::new("P-001")),
1721 Some(Money::new(20.0, usd)),
1722 );
1723 let _state = manager.update_balances(
1724 AccountAny::Cash(account),
1725 &InstrumentAny::CurrencyPair(instrument),
1726 fill2,
1727 );
1728
1729 let account_after = cache
1730 .borrow()
1731 .account(&AccountId::new("SIM-001"))
1732 .unwrap()
1733 .clone();
1734
1735 if let AccountAny::Cash(cash) = account_after {
1736 assert_eq!(cash.balance_total(Some(usd)), Some(initial_balance));
1737 } else {
1738 panic!("Expected CashAccount");
1739 }
1740 }
1741
1742 #[rstest]
1743 fn test_order_canceled_releases_locked_balance() {
1744 let usd = Currency::USD();
1746 let account_state = AccountState::new(
1747 AccountId::new("SIM-001"),
1748 AccountType::Cash,
1749 vec![AccountBalance::new(
1750 Money::new(100_000.0, usd),
1751 Money::new(0.0, usd),
1752 Money::new(100_000.0, usd),
1753 )],
1754 Vec::new(),
1755 true,
1756 UUID4::new(),
1757 UnixNanos::default(),
1758 UnixNanos::default(),
1759 Some(usd),
1760 );
1761
1762 let account = CashAccount::new(account_state, true, false);
1763
1764 let clock = Rc::new(RefCell::new(TestClock::new()));
1765 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1766 cache
1767 .borrow_mut()
1768 .add_account(AccountAny::Cash(account.clone()))
1769 .unwrap();
1770
1771 let manager = AccountsManager::new(clock, cache);
1772 let instrument = audusd_sim();
1773
1774 let mut order = OrderTestBuilder::new(OrderType::Limit)
1775 .instrument_id(instrument.id())
1776 .side(OrderSide::Buy)
1777 .quantity(Quantity::from("100000"))
1778 .price(Price::from("0.80000"))
1779 .build();
1780
1781 let submitted = OrderSubmitted::new(
1782 order.trader_id(),
1783 order.strategy_id(),
1784 order.instrument_id(),
1785 order.client_order_id(),
1786 AccountId::new("SIM-001"),
1787 UUID4::new(),
1788 UnixNanos::default(),
1789 UnixNanos::default(),
1790 );
1791
1792 let accepted = OrderAccepted::new(
1793 order.trader_id(),
1794 order.strategy_id(),
1795 order.instrument_id(),
1796 order.client_order_id(),
1797 order.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1798 AccountId::new("SIM-001"),
1799 UUID4::new(),
1800 UnixNanos::default(),
1801 UnixNanos::default(),
1802 false,
1803 );
1804
1805 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1806 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1807
1808 let result = manager.update_orders(
1809 &AccountAny::Cash(account),
1810 &InstrumentAny::CurrencyPair(instrument.clone()),
1811 vec![&order],
1812 UnixNanos::default(),
1813 );
1814
1815 assert!(result.is_some());
1816 let (updated_account, _) = result.unwrap();
1817
1818 if let AccountAny::Cash(ref cash) = updated_account {
1819 assert_eq!(
1821 cash.balance_locked(Some(usd)),
1822 Some(Money::new(80_000.0, usd))
1823 );
1824 assert_eq!(
1825 cash.balance_free(Some(usd)),
1826 Some(Money::new(20_000.0, usd))
1827 );
1828 } else {
1829 panic!("Expected CashAccount");
1830 }
1831
1832 let result = manager.update_orders(
1833 &updated_account,
1834 &InstrumentAny::CurrencyPair(instrument),
1835 vec![],
1836 UnixNanos::default(),
1837 );
1838
1839 assert!(result.is_some());
1840 let (final_account, _) = result.unwrap();
1841
1842 if let AccountAny::Cash(cash) = final_account {
1843 assert_eq!(cash.balance_locked(Some(usd)), Some(Money::new(0.0, usd)));
1844 assert_eq!(
1845 cash.balance_free(Some(usd)),
1846 Some(Money::new(100_000.0, usd))
1847 );
1848 assert_eq!(
1849 cash.balance_total(Some(usd)),
1850 Some(Money::new(100_000.0, usd))
1851 );
1852 } else {
1853 panic!("Expected CashAccount");
1854 }
1855 }
1856
1857 #[rstest]
1858 fn test_generate_account_state_preserves_per_instrument_and_account_wide_margins() {
1859 let usd = Currency::USD();
1860 let audusd = InstrumentId::from("AUD/USD.SIM");
1861 let account_state = AccountState::new(
1862 AccountId::new("SIM-001"),
1863 AccountType::Margin,
1864 vec![AccountBalance::new(
1865 Money::new(1_000_000.0, usd),
1866 Money::new(0.0, usd),
1867 Money::new(1_000_000.0, usd),
1868 )],
1869 Vec::new(),
1870 true,
1871 UUID4::new(),
1872 UnixNanos::default(),
1873 UnixNanos::default(),
1874 Some(usd),
1875 );
1876 let mut account = MarginAccount::new(account_state, false);
1877 account.update_margin(MarginBalance::new(
1878 Money::new(150.0, usd),
1879 Money::new(75.0, usd),
1880 Some(audusd),
1881 ));
1882 account.update_margin(MarginBalance::new(
1883 Money::new(500.0, usd),
1884 Money::new(250.0, usd),
1885 None,
1886 ));
1887
1888 let clock = Rc::new(RefCell::new(TestClock::new()));
1889 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1890 let manager = AccountsManager::new(clock, cache);
1891
1892 let state =
1893 manager.generate_account_state(AccountAny::Margin(account), UnixNanos::default());
1894
1895 assert_eq!(state.margins.len(), 2);
1896 let per_instrument: Vec<_> = state
1897 .margins
1898 .iter()
1899 .filter(|m| m.instrument_id.is_some())
1900 .collect();
1901 let account_wide: Vec<_> = state
1902 .margins
1903 .iter()
1904 .filter(|m| m.instrument_id.is_none())
1905 .collect();
1906 assert_eq!(per_instrument.len(), 1);
1907 assert_eq!(per_instrument[0].instrument_id, Some(audusd));
1908 assert_eq!(per_instrument[0].initial, Money::new(150.0, usd));
1909 assert_eq!(per_instrument[0].maintenance, Money::new(75.0, usd));
1910 assert_eq!(account_wide.len(), 1);
1911 assert_eq!(account_wide[0].currency, usd);
1912 assert_eq!(account_wide[0].initial, Money::new(500.0, usd));
1913 assert_eq!(account_wide[0].maintenance, Money::new(250.0, usd));
1914 }
1915}