1use std::{cell::RefCell, 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, Money},
30};
31use rust_decimal::{Decimal, prelude::ToPrimitive};
32pub struct AccountsManager {
37 clock: Rc<RefCell<dyn Clock>>,
38 cache: Rc<RefCell<Cache>>,
39}
40
41impl Debug for AccountsManager {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct(stringify!(AccountsManager)).finish()
44 }
45}
46
47impl AccountsManager {
48 pub fn new(clock: Rc<RefCell<dyn Clock>>, cache: Rc<RefCell<Cache>>) -> Self {
50 Self { clock, cache }
51 }
52
53 #[must_use]
59 pub fn update_balances(
60 &self,
61 account: AccountAny,
62 instrument: InstrumentAny,
63 fill: OrderFilled,
64 ) -> AccountState {
65 let cache = self.cache.borrow();
66 let position_id = if let Some(position_id) = fill.position_id {
67 position_id
68 } else {
69 let positions_open = cache.positions_open(None, Some(&fill.instrument_id), None, None);
70 positions_open
71 .first()
72 .unwrap_or_else(|| panic!("List of Positions is empty"))
73 .id
74 };
75
76 let position = cache.position(&position_id);
77
78 let pnls = account.calculate_pnls(instrument, fill, position.cloned());
79
80 match account.base_currency() {
82 Some(base_currency) => {
83 let pnl = pnls.map_or_else(
84 |_| Money::new(0.0, base_currency),
85 |pnl_list| {
86 pnl_list
87 .first()
88 .copied()
89 .unwrap_or_else(|| Money::new(0.0, base_currency))
90 },
91 );
92
93 self.update_balance_single_currency(account.clone(), &fill, pnl);
94 }
95 None => {
96 if let Ok(mut pnl_list) = pnls {
97 self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list);
98 }
99 }
100 }
101
102 self.generate_account_state(account, fill.ts_event)
104 }
105
106 #[must_use]
111 pub fn update_orders(
112 &self,
113 account: &AccountAny,
114 instrument: InstrumentAny,
115 orders_open: Vec<&OrderAny>,
116 ts_event: UnixNanos,
117 ) -> Option<(AccountAny, AccountState)> {
118 match account.clone() {
119 AccountAny::Cash(cash_account) => self
120 .update_balance_locked(&cash_account, instrument, orders_open, ts_event)
121 .map(|(updated_cash_account, state)| {
122 (AccountAny::Cash(updated_cash_account), state)
123 }),
124 AccountAny::Margin(margin_account) => self
125 .update_margin_init(&margin_account, instrument, orders_open, ts_event)
126 .map(|(updated_margin_account, state)| {
127 (AccountAny::Margin(updated_margin_account), state)
128 }),
129 }
130 }
131
132 #[must_use]
138 pub fn update_positions(
139 &self,
140 account: &MarginAccount,
141 instrument: InstrumentAny,
142 positions: Vec<&Position>,
143 ts_event: UnixNanos,
144 ) -> Option<(MarginAccount, AccountState)> {
145 let mut total_margin_maint = 0.0;
146 let mut base_xrate: Option<f64> = None;
147 let mut currency = instrument.settlement_currency();
148 let mut account = account.clone();
149
150 for position in positions {
151 assert_eq!(
152 position.instrument_id,
153 instrument.id(),
154 "Position not for instrument {}",
155 instrument.id()
156 );
157
158 if !position.is_open() {
159 continue;
160 }
161
162 let margin_maint = match instrument {
163 InstrumentAny::Betting(i) => account.calculate_maintenance_margin(
164 i,
165 position.quantity,
166 instrument.make_price(position.avg_px_open),
167 None,
168 ),
169 InstrumentAny::BinaryOption(i) => account.calculate_maintenance_margin(
170 i,
171 position.quantity,
172 instrument.make_price(position.avg_px_open),
173 None,
174 ),
175 InstrumentAny::CryptoFuture(i) => account.calculate_maintenance_margin(
176 i,
177 position.quantity,
178 instrument.make_price(position.avg_px_open),
179 None,
180 ),
181 InstrumentAny::CryptoOption(i) => account.calculate_maintenance_margin(
182 i,
183 position.quantity,
184 instrument.make_price(position.avg_px_open),
185 None,
186 ),
187 InstrumentAny::CryptoPerpetual(i) => account.calculate_maintenance_margin(
188 i,
189 position.quantity,
190 instrument.make_price(position.avg_px_open),
191 None,
192 ),
193 InstrumentAny::CurrencyPair(i) => account.calculate_maintenance_margin(
194 i,
195 position.quantity,
196 instrument.make_price(position.avg_px_open),
197 None,
198 ),
199 InstrumentAny::Equity(i) => account.calculate_maintenance_margin(
200 i,
201 position.quantity,
202 instrument.make_price(position.avg_px_open),
203 None,
204 ),
205 InstrumentAny::FuturesContract(i) => account.calculate_maintenance_margin(
206 i,
207 position.quantity,
208 instrument.make_price(position.avg_px_open),
209 None,
210 ),
211 InstrumentAny::FuturesSpread(i) => account.calculate_maintenance_margin(
212 i,
213 position.quantity,
214 instrument.make_price(position.avg_px_open),
215 None,
216 ),
217 InstrumentAny::OptionContract(i) => account.calculate_maintenance_margin(
218 i,
219 position.quantity,
220 instrument.make_price(position.avg_px_open),
221 None,
222 ),
223 InstrumentAny::OptionSpread(i) => account.calculate_maintenance_margin(
224 i,
225 position.quantity,
226 instrument.make_price(position.avg_px_open),
227 None,
228 ),
229 };
230
231 let mut margin_maint = margin_maint.as_f64();
232
233 if let Some(base_currency) = account.base_currency {
234 if base_xrate.is_none() {
235 currency = base_currency;
236 base_xrate = self.calculate_xrate_to_base(
237 AccountAny::Margin(account.clone()),
238 instrument.clone(),
239 position.entry.as_specified(),
240 );
241 }
242
243 if let Some(xrate) = base_xrate {
244 margin_maint *= xrate;
245 } else {
246 log::debug!(
247 "Cannot calculate maintenance (position) margin: insufficient data for {}/{}",
248 instrument.settlement_currency(),
249 base_currency
250 );
251 return None;
252 }
253 }
254
255 total_margin_maint += margin_maint;
256 }
257
258 let margin_maint = Money::new(total_margin_maint, currency);
259 account.update_maintenance_margin(instrument.id(), margin_maint);
260
261 log::info!("{} margin_maint={margin_maint}", instrument.id());
262
263 Some((
265 account.clone(),
266 self.generate_account_state(AccountAny::Margin(account), ts_event),
267 ))
268 }
269
270 fn update_balance_locked(
271 &self,
272 account: &CashAccount,
273 instrument: InstrumentAny,
274 orders_open: Vec<&OrderAny>,
275 ts_event: UnixNanos,
276 ) -> Option<(CashAccount, AccountState)> {
277 let mut account = account.clone();
278 if orders_open.is_empty() {
279 let balance = account.balances.remove(&instrument.quote_currency());
280 if let Some(balance) = balance {
281 account.recalculate_balance(balance.currency);
282 }
283 return Some((
284 account.clone(),
285 self.generate_account_state(AccountAny::Cash(account), ts_event),
286 ));
287 }
288
289 let mut total_locked = 0.0;
290 let mut base_xrate: Option<f64> = None;
291
292 let mut currency = instrument.settlement_currency();
293
294 for order in orders_open {
295 assert_eq!(
296 order.instrument_id(),
297 instrument.id(),
298 "Order not for instrument {}",
299 instrument.id()
300 );
301 assert!(order.is_open(), "Order is not open");
302
303 if order.price().is_none() && order.trigger_price().is_none() {
304 continue;
305 }
306
307 if order.is_reduce_only() {
308 continue; }
310
311 let price = if order.price().is_some() {
312 order.price()
313 } else {
314 order.trigger_price()
315 };
316
317 let mut locked = account
318 .calculate_balance_locked(
319 instrument.clone(),
320 order.order_side(),
321 order.quantity(),
322 price?,
323 None,
324 )
325 .unwrap()
326 .as_f64();
327
328 if let Some(base_curr) = account.base_currency() {
329 if base_xrate.is_none() {
330 currency = base_curr;
331 base_xrate = self.calculate_xrate_to_base(
332 AccountAny::Cash(account.clone()),
333 instrument.clone(),
334 order.order_side_specified(),
335 );
336 }
337
338 if let Some(xrate) = base_xrate {
339 locked *= xrate;
340 } else {
341 panic!("Cannot calculate base xrate");
343 }
344 }
345
346 total_locked += locked;
347 }
348
349 let balance_locked = Money::new(total_locked.to_f64()?, currency);
350
351 if let Some(balance) = account.balances.get_mut(&instrument.quote_currency()) {
352 balance.locked = balance_locked;
353 let currency = balance.currency;
354 account.recalculate_balance(currency);
355 }
356
357 log::info!("{} balance_locked={balance_locked}", instrument.id());
358
359 Some((
360 account.clone(),
361 self.generate_account_state(AccountAny::Cash(account), ts_event),
362 ))
363 }
364
365 fn update_margin_init(
366 &self,
367 account: &MarginAccount,
368 instrument: InstrumentAny,
369 orders_open: Vec<&OrderAny>,
370 ts_event: UnixNanos,
371 ) -> Option<(MarginAccount, AccountState)> {
372 let mut total_margin_init = 0.0;
373 let mut base_xrate: Option<f64> = None;
374 let mut currency = instrument.settlement_currency();
375 let mut account = account.clone();
376
377 for order in orders_open {
378 assert_eq!(
379 order.instrument_id(),
380 instrument.id(),
381 "Order not for instrument {}",
382 instrument.id()
383 );
384
385 if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) {
386 continue;
387 }
388
389 if order.is_reduce_only() {
390 continue; }
392
393 let price = if order.price().is_some() {
394 order.price()
395 } else {
396 order.trigger_price()
397 };
398
399 let margin_init = match instrument {
400 InstrumentAny::Betting(i) => {
401 account.calculate_initial_margin(i, order.quantity(), price?, None)
402 }
403 InstrumentAny::BinaryOption(i) => {
404 account.calculate_initial_margin(i, order.quantity(), price?, None)
405 }
406 InstrumentAny::CryptoFuture(i) => {
407 account.calculate_initial_margin(i, order.quantity(), price?, None)
408 }
409 InstrumentAny::CryptoOption(i) => {
410 account.calculate_initial_margin(i, order.quantity(), price?, None)
411 }
412 InstrumentAny::CryptoPerpetual(i) => {
413 account.calculate_initial_margin(i, order.quantity(), price?, None)
414 }
415 InstrumentAny::CurrencyPair(i) => {
416 account.calculate_initial_margin(i, order.quantity(), price?, None)
417 }
418 InstrumentAny::Equity(i) => {
419 account.calculate_initial_margin(i, order.quantity(), price?, None)
420 }
421 InstrumentAny::FuturesContract(i) => {
422 account.calculate_initial_margin(i, order.quantity(), price?, None)
423 }
424 InstrumentAny::FuturesSpread(i) => {
425 account.calculate_initial_margin(i, order.quantity(), price?, None)
426 }
427 InstrumentAny::OptionContract(i) => {
428 account.calculate_initial_margin(i, order.quantity(), price?, None)
429 }
430 InstrumentAny::OptionSpread(i) => {
431 account.calculate_initial_margin(i, order.quantity(), price?, None)
432 }
433 };
434
435 let mut margin_init = margin_init.as_f64();
436
437 if let Some(base_currency) = account.base_currency {
438 if base_xrate.is_none() {
439 currency = base_currency;
440 base_xrate = self.calculate_xrate_to_base(
441 AccountAny::Margin(account.clone()),
442 instrument.clone(),
443 order.order_side_specified(),
444 );
445 }
446
447 if let Some(xrate) = base_xrate {
448 margin_init *= xrate;
449 } else {
450 log::debug!(
451 "Cannot calculate initial margin: insufficient data for {}/{}",
452 instrument.settlement_currency(),
453 base_currency
454 );
455 continue;
456 }
457 }
458
459 total_margin_init += margin_init;
460 }
461
462 let money = Money::new(total_margin_init, currency);
463 let margin_init = {
464 account.update_initial_margin(instrument.id(), money);
465 money
466 };
467
468 log::info!("{} margin_init={margin_init}", instrument.id());
469
470 Some((
471 account.clone(),
472 self.generate_account_state(AccountAny::Margin(account), ts_event),
473 ))
474 }
475
476 fn update_balance_single_currency(
477 &self,
478 account: AccountAny,
479 fill: &OrderFilled,
480 mut pnl: Money,
481 ) {
482 let base_currency = if let Some(currency) = account.base_currency() {
483 currency
484 } else {
485 log::error!("Account has no base currency set");
486 return;
487 };
488
489 let mut balances = Vec::new();
490 let mut commission = fill.commission;
491
492 if let Some(ref mut comm) = commission {
493 if comm.currency != base_currency {
494 let xrate = self.cache.borrow().get_xrate(
495 fill.instrument_id.venue,
496 comm.currency,
497 base_currency,
498 if fill.order_side == OrderSide::Sell {
499 PriceType::Bid
500 } else {
501 PriceType::Ask
502 },
503 );
504
505 if let Some(xrate) = xrate {
506 *comm = Money::new(comm.as_f64() * xrate, base_currency);
507 } else {
508 log::error!(
509 "Cannot calculate account state: insufficient data for {}/{}",
510 comm.currency,
511 base_currency
512 );
513 return;
514 }
515 }
516 }
517
518 if pnl.currency != base_currency {
519 let xrate = self.cache.borrow().get_xrate(
520 fill.instrument_id.venue,
521 pnl.currency,
522 base_currency,
523 if fill.order_side == OrderSide::Sell {
524 PriceType::Bid
525 } else {
526 PriceType::Ask
527 },
528 );
529
530 if let Some(xrate) = xrate {
531 pnl = Money::new(pnl.as_f64() * xrate, base_currency);
532 } else {
533 log::error!(
534 "Cannot calculate account state: insufficient data for {}/{}",
535 pnl.currency,
536 base_currency
537 );
538 return;
539 }
540 }
541
542 if let Some(comm) = commission {
543 pnl -= comm;
544 }
545
546 if pnl.is_zero() {
547 return;
548 }
549
550 let existing_balances = account.balances();
551 let balance = if let Some(b) = existing_balances.get(&pnl.currency) {
552 b
553 } else {
554 log::error!(
555 "Cannot complete transaction: no balance for {}",
556 pnl.currency
557 );
558 return;
559 };
560
561 let new_balance =
562 AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl);
563 balances.push(new_balance);
564
565 match account {
566 AccountAny::Cash(mut cash) => {
567 cash.update_balances(balances);
568 if let Some(comm) = commission {
569 cash.update_commissions(comm);
570 }
571 }
572 AccountAny::Margin(mut margin) => {
573 margin.update_balances(balances);
574 if let Some(comm) = commission {
575 margin.update_commissions(comm);
576 }
577 }
578 }
579 }
580
581 fn update_balance_multi_currency(
582 &self,
583 account: AccountAny,
584 fill: OrderFilled,
585 pnls: &mut [Money],
586 ) {
587 let mut new_balances = Vec::new();
588 let commission = fill.commission;
589 let mut apply_commission = commission.is_some_and(|c| !c.is_zero());
590
591 for pnl in pnls.iter_mut() {
592 if apply_commission && pnl.currency == commission.unwrap().currency {
593 *pnl -= commission.unwrap();
594 apply_commission = false;
595 }
596
597 if pnl.is_zero() {
598 continue; }
600
601 let currency = pnl.currency;
602 let balances = account.balances();
603
604 let new_balance = if let Some(balance) = balances.get(¤cy) {
605 let new_total = balance.total.as_f64() + pnl.as_f64();
606 let new_free = balance.free.as_f64() + pnl.as_f64();
607 let total = Money::new(new_total, currency);
608 let free = Money::new(new_free, currency);
609
610 if new_total < 0.0 {
611 log::error!(
612 "AccountBalanceNegative: balance = {}, currency = {}",
613 total.as_decimal(),
614 currency
615 );
616 return;
617 }
618 if new_free < 0.0 {
619 log::error!(
620 "AccountMarginExceeded: balance = {}, margin = {}, currency = {}",
621 total.as_decimal(),
622 balance.locked.as_decimal(),
623 currency
624 );
625 return;
626 }
627
628 AccountBalance::new(total, balance.locked, free)
629 } else {
630 if pnl.as_decimal() < Decimal::ZERO {
631 log::error!(
632 "Cannot complete transaction: no {currency} to deduct a {pnl} realized PnL from"
633 );
634 return;
635 }
636 AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl)
637 };
638
639 new_balances.push(new_balance);
640 }
641
642 if apply_commission {
643 let commission = commission.unwrap();
644 let currency = commission.currency;
645 let balances = account.balances();
646
647 let commission_balance = if let Some(balance) = balances.get(¤cy) {
648 let new_total = balance.total.as_decimal() - commission.as_decimal();
649 let new_free = balance.free.as_decimal() - commission.as_decimal();
650 AccountBalance::new(
651 Money::new(new_total.to_f64().unwrap(), currency),
652 balance.locked,
653 Money::new(new_free.to_f64().unwrap(), currency),
654 )
655 } else {
656 if commission.as_decimal() > Decimal::ZERO {
657 log::error!(
658 "Cannot complete transaction: no {currency} balance to deduct a {commission} commission from"
659 );
660 return;
661 }
662 AccountBalance::new(
663 Money::new(0.0, currency),
664 Money::new(0.0, currency),
665 Money::new(0.0, currency),
666 )
667 };
668 new_balances.push(commission_balance);
669 }
670
671 if new_balances.is_empty() {
672 return;
673 }
674
675 match account {
676 AccountAny::Cash(mut cash) => {
677 cash.update_balances(new_balances);
678 if let Some(commission) = commission {
679 cash.update_commissions(commission);
680 }
681 }
682 AccountAny::Margin(mut margin) => {
683 margin.update_balances(new_balances);
684 if let Some(commission) = commission {
685 margin.update_commissions(commission);
686 }
687 }
688 }
689 }
690
691 fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState {
692 match account {
693 AccountAny::Cash(cash_account) => AccountState::new(
694 cash_account.id,
695 AccountType::Cash,
696 cash_account.balances.clone().into_values().collect(),
697 vec![],
698 false,
699 UUID4::new(),
700 ts_event,
701 self.clock.borrow().timestamp_ns(),
702 cash_account.base_currency(),
703 ),
704 AccountAny::Margin(margin_account) => AccountState::new(
705 margin_account.id,
706 AccountType::Cash,
707 vec![],
708 margin_account.margins.clone().into_values().collect(),
709 false,
710 UUID4::new(),
711 ts_event,
712 self.clock.borrow().timestamp_ns(),
713 margin_account.base_currency(),
714 ),
715 }
716 }
717
718 fn calculate_xrate_to_base(
719 &self,
720 account: AccountAny,
721 instrument: InstrumentAny,
722 side: OrderSideSpecified,
723 ) -> Option<f64> {
724 match account.base_currency() {
725 None => Some(1.0),
726 Some(base_curr) => self.cache.borrow().get_xrate(
727 instrument.id().venue,
728 instrument.settlement_currency(),
729 base_curr,
730 match side {
731 OrderSideSpecified::Sell => PriceType::Bid,
732 OrderSideSpecified::Buy => PriceType::Ask,
733 },
734 ),
735 }
736 }
737}