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