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