nautilus_portfolio/
manager.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Provides account management functionality.
17
18use 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        // Calculate final PnL including commissions
65        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        // Generate and return account state
87        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        // Generate and return account state
233        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; // No Adjustment
560            }
561
562            let currency = pnl.currency;
563            let balances = account.balances();
564
565            let new_balance = if let Some(balance) = balances.get(&currency) {
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(&currency) {
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}