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