Skip to main content

nautilus_portfolio/
manager.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
21use nautilus_common::{cache::Cache, clock::Clock};
22use nautilus_core::{UUID4, UnixNanos};
23use nautilus_model::{
24    accounts::{Account, AccountAny, BettingAccount, CashAccount, MarginAccount},
25    enums::{AccountType, OrderSide, PriceType},
26    events::{AccountState, OrderFilled},
27    instruments::{Instrument, InstrumentAny},
28    orders::{Order, OrderAny},
29    position::Position,
30    types::{AccountBalance, Currency, Money},
31};
32use rust_decimal::{Decimal, prelude::ToPrimitive};
33
34/// Manages account balance updates and calculations for portfolio management.
35///
36/// The accounts manager handles balance updates for different account types,
37/// including cash and margin accounts, based on order fills and position changes.
38pub struct AccountsManager {
39    clock: Rc<RefCell<dyn Clock>>,
40    cache: Rc<RefCell<Cache>>,
41}
42
43impl Debug for AccountsManager {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct(stringify!(AccountsManager)).finish()
46    }
47}
48
49impl AccountsManager {
50    /// Creates a new [`AccountsManager`] instance.
51    pub fn new(clock: Rc<RefCell<dyn Clock>>, cache: Rc<RefCell<Cache>>) -> Self {
52        Self { clock, cache }
53    }
54
55    /// Updates the given account state based on a filled order.
56    ///
57    /// # Panics
58    ///
59    /// Panics if the position list for the filled instrument is empty.
60    #[must_use]
61    pub fn update_balances(
62        &self,
63        account: AccountAny,
64        instrument: &InstrumentAny,
65        fill: OrderFilled,
66    ) -> AccountState {
67        let cache = self.cache.borrow();
68        let position_id = if let Some(position_id) = fill.position_id {
69            position_id
70        } else {
71            let positions_open = cache.positions_open(
72                None,
73                Some(&fill.instrument_id),
74                None,
75                Some(&fill.account_id),
76                None,
77            );
78            positions_open
79                .first()
80                .unwrap_or_else(|| panic!("List of Positions is empty"))
81                .id
82        };
83
84        let position = cache.position(&position_id);
85
86        let pnls = account.calculate_pnls(instrument, &fill, position.cloned());
87
88        // Calculate final PnL including commissions
89        match account.base_currency() {
90            Some(base_currency) => {
91                let pnl = pnls.map_or_else(
92                    |_| Money::new(0.0, base_currency),
93                    |pnl_list| {
94                        pnl_list
95                            .first()
96                            .copied()
97                            .unwrap_or_else(|| Money::new(0.0, base_currency))
98                    },
99                );
100
101                self.update_balance_single_currency(account.clone(), &fill, pnl);
102            }
103            None => {
104                if let Ok(mut pnl_list) = pnls {
105                    self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list);
106                }
107            }
108        }
109
110        // Generate and return account state
111        self.generate_account_state(account, fill.ts_event)
112    }
113
114    /// Updates account balances based on open orders.
115    ///
116    /// For cash accounts, updates the balance locked by open orders.
117    /// For margin accounts, updates the initial margin requirements.
118    #[must_use]
119    pub fn update_orders(
120        &self,
121        account: &AccountAny,
122        instrument: &InstrumentAny,
123        orders_open: Vec<&OrderAny>,
124        ts_event: UnixNanos,
125    ) -> Option<(AccountAny, AccountState)> {
126        match account.clone() {
127            AccountAny::Margin(margin_account) => self
128                .update_margin_init(&margin_account, instrument, orders_open, ts_event)
129                .map(|(updated_margin_account, state)| {
130                    (AccountAny::Margin(updated_margin_account), state)
131                }),
132            AccountAny::Cash(cash_account) => self
133                .update_balance_locked(&cash_account, instrument, &orders_open, ts_event)
134                .map(|(updated_cash_account, state)| {
135                    (AccountAny::Cash(updated_cash_account), state)
136                }),
137            AccountAny::Betting(betting_account) => self
138                .update_balance_locked_betting(&betting_account, instrument, &orders_open, ts_event)
139                .map(|(updated_betting_account, state)| {
140                    (AccountAny::Betting(updated_betting_account), state)
141                }),
142        }
143    }
144
145    /// Updates the account based on current open positions.
146    ///
147    /// # Panics
148    ///
149    /// Panics if any position's `instrument_id` does not match the provided `instrument`.
150    #[must_use]
151    pub fn update_positions(
152        &self,
153        account: &MarginAccount,
154        instrument: &InstrumentAny,
155        positions: Vec<&Position>,
156        ts_event: UnixNanos,
157    ) -> Option<(MarginAccount, AccountState)> {
158        let mut total_margin_maint = 0.0;
159        let mut base_xrate: Option<f64> = None;
160        let mut currency = instrument.settlement_currency();
161        let mut account = account.clone();
162
163        for position in positions {
164            assert_eq!(
165                position.instrument_id,
166                instrument.id(),
167                "Position not for instrument {}",
168                instrument.id()
169            );
170
171            if !position.is_open() {
172                continue;
173            }
174
175            let margin_maint = match instrument {
176                InstrumentAny::Betting(i) => account
177                    .calculate_maintenance_margin(
178                        i,
179                        position.quantity,
180                        instrument.make_price(position.avg_px_open),
181                        None,
182                    )
183                    .ok()?,
184                InstrumentAny::BinaryOption(i) => account
185                    .calculate_maintenance_margin(
186                        i,
187                        position.quantity,
188                        instrument.make_price(position.avg_px_open),
189                        None,
190                    )
191                    .ok()?,
192                InstrumentAny::Cfd(i) => account
193                    .calculate_maintenance_margin(
194                        i,
195                        position.quantity,
196                        instrument.make_price(position.avg_px_open),
197                        None,
198                    )
199                    .ok()?,
200                InstrumentAny::Commodity(i) => account
201                    .calculate_maintenance_margin(
202                        i,
203                        position.quantity,
204                        instrument.make_price(position.avg_px_open),
205                        None,
206                    )
207                    .ok()?,
208                InstrumentAny::CryptoFuture(i) => account
209                    .calculate_maintenance_margin(
210                        i,
211                        position.quantity,
212                        instrument.make_price(position.avg_px_open),
213                        None,
214                    )
215                    .ok()?,
216                InstrumentAny::CryptoOption(i) => account
217                    .calculate_maintenance_margin(
218                        i,
219                        position.quantity,
220                        instrument.make_price(position.avg_px_open),
221                        None,
222                    )
223                    .ok()?,
224                InstrumentAny::CryptoPerpetual(i) => account
225                    .calculate_maintenance_margin(
226                        i,
227                        position.quantity,
228                        instrument.make_price(position.avg_px_open),
229                        None,
230                    )
231                    .ok()?,
232                InstrumentAny::CurrencyPair(i) => account
233                    .calculate_maintenance_margin(
234                        i,
235                        position.quantity,
236                        instrument.make_price(position.avg_px_open),
237                        None,
238                    )
239                    .ok()?,
240                InstrumentAny::Equity(i) => account
241                    .calculate_maintenance_margin(
242                        i,
243                        position.quantity,
244                        instrument.make_price(position.avg_px_open),
245                        None,
246                    )
247                    .ok()?,
248                InstrumentAny::FuturesContract(i) => account
249                    .calculate_maintenance_margin(
250                        i,
251                        position.quantity,
252                        instrument.make_price(position.avg_px_open),
253                        None,
254                    )
255                    .ok()?,
256                InstrumentAny::FuturesSpread(i) => account
257                    .calculate_maintenance_margin(
258                        i,
259                        position.quantity,
260                        instrument.make_price(position.avg_px_open),
261                        None,
262                    )
263                    .ok()?,
264                InstrumentAny::IndexInstrument(i) => account
265                    .calculate_maintenance_margin(
266                        i,
267                        position.quantity,
268                        instrument.make_price(position.avg_px_open),
269                        None,
270                    )
271                    .ok()?,
272                InstrumentAny::OptionContract(i) => account
273                    .calculate_maintenance_margin(
274                        i,
275                        position.quantity,
276                        instrument.make_price(position.avg_px_open),
277                        None,
278                    )
279                    .ok()?,
280                InstrumentAny::OptionSpread(i) => account
281                    .calculate_maintenance_margin(
282                        i,
283                        position.quantity,
284                        instrument.make_price(position.avg_px_open),
285                        None,
286                    )
287                    .ok()?,
288                InstrumentAny::PerpetualContract(i) => account
289                    .calculate_maintenance_margin(
290                        i,
291                        position.quantity,
292                        instrument.make_price(position.avg_px_open),
293                        None,
294                    )
295                    .ok()?,
296                InstrumentAny::TokenizedAsset(i) => account
297                    .calculate_maintenance_margin(
298                        i,
299                        position.quantity,
300                        instrument.make_price(position.avg_px_open),
301                        None,
302                    )
303                    .ok()?,
304            };
305
306            let mut margin_maint = margin_maint.as_f64();
307
308            if let Some(base_currency) = account.base_currency {
309                if base_xrate.is_none() {
310                    currency = base_currency;
311                    base_xrate = self
312                        .calculate_xrate_to_base(&AccountAny::Margin(account.clone()), instrument);
313                }
314
315                if let Some(xrate) = base_xrate {
316                    margin_maint *= xrate;
317                } else {
318                    log::debug!(
319                        "Cannot calculate maintenance (position) margin: insufficient data for {}/{}",
320                        instrument.settlement_currency(),
321                        base_currency
322                    );
323                    return None;
324                }
325            }
326
327            total_margin_maint += margin_maint;
328        }
329
330        let margin_maint = Money::new(total_margin_maint, currency);
331        account.update_maintenance_margin(instrument.id(), margin_maint);
332
333        log::info!("{} margin_maint={margin_maint}", instrument.id());
334
335        // Generate and return account state
336        Some((
337            account.clone(),
338            self.generate_account_state(AccountAny::Margin(account), ts_event),
339        ))
340    }
341
342    fn update_balance_locked(
343        &self,
344        account: &CashAccount,
345        instrument: &InstrumentAny,
346        orders_open: &[&OrderAny],
347        ts_event: UnixNanos,
348    ) -> Option<(CashAccount, AccountState)> {
349        let mut account = account.clone();
350
351        if orders_open.is_empty() {
352            account.clear_balance_locked(instrument.id());
353            return Some((
354                account.clone(),
355                self.generate_account_state(AccountAny::Cash(account), ts_event),
356            ));
357        }
358
359        let mut total_locked: AHashMap<Currency, Money> = AHashMap::new();
360        let mut base_xrate: Option<f64> = None;
361
362        let mut currency = instrument.settlement_currency();
363
364        for order in orders_open {
365            assert_eq!(
366                order.instrument_id(),
367                instrument.id(),
368                "Order not for instrument {}",
369                instrument.id()
370            );
371            assert!(order.is_open(), "Order is not open");
372
373            if order.price().is_none() && order.trigger_price().is_none() {
374                continue;
375            }
376
377            if order.is_reduce_only() {
378                continue; // Does not contribute to locked balance
379            }
380
381            let price = if order.price().is_some() {
382                order.price()
383            } else {
384                order.trigger_price()
385            };
386
387            let mut locked = account
388                .calculate_balance_locked(
389                    instrument,
390                    order.order_side(),
391                    order.quantity(),
392                    price?,
393                    None,
394                )
395                .unwrap();
396
397            if let Some(base_curr) = account.base_currency() {
398                if base_xrate.is_none() {
399                    currency = base_curr;
400                    base_xrate = self
401                        .calculate_xrate_to_base(&AccountAny::Cash(account.clone()), instrument);
402                }
403
404                if let Some(xrate) = base_xrate {
405                    locked = Money::new(locked.as_f64() * xrate, currency);
406                } else {
407                    log::error!(
408                        "Cannot calculate balance locked: insufficient data for {}/{}",
409                        instrument.settlement_currency(),
410                        base_curr
411                    );
412                    return None;
413                }
414            }
415
416            total_locked
417                .entry(locked.currency)
418                .and_modify(|total| *total = *total + locked)
419                .or_insert(locked);
420        }
421
422        if total_locked.is_empty() {
423            account.clear_balance_locked(instrument.id());
424            return Some((
425                account.clone(),
426                self.generate_account_state(AccountAny::Cash(account), ts_event),
427            ));
428        }
429
430        // Clear existing locks before applying new ones to remove stale currency entries
431        account.clear_balance_locked(instrument.id());
432
433        for (_, balance_locked) in total_locked {
434            account.update_balance_locked(instrument.id(), balance_locked);
435            log::info!("{} balance_locked={balance_locked}", instrument.id());
436        }
437
438        Some((
439            account.clone(),
440            self.generate_account_state(AccountAny::Cash(account), ts_event),
441        ))
442    }
443
444    fn update_margin_init(
445        &self,
446        account: &MarginAccount,
447        instrument: &InstrumentAny,
448        orders_open: Vec<&OrderAny>,
449        ts_event: UnixNanos,
450    ) -> Option<(MarginAccount, AccountState)> {
451        let mut total_margin_init = 0.0;
452        let mut base_xrate: Option<f64> = None;
453        let mut currency = instrument.settlement_currency();
454        let mut account = account.clone();
455
456        for order in orders_open {
457            assert_eq!(
458                order.instrument_id(),
459                instrument.id(),
460                "Order not for instrument {}",
461                instrument.id()
462            );
463
464            if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) {
465                continue;
466            }
467
468            if order.is_reduce_only() {
469                continue; // Does not contribute to margin
470            }
471
472            let price = if order.price().is_some() {
473                order.price()
474            } else {
475                order.trigger_price()
476            };
477
478            let margin_init = match instrument {
479                InstrumentAny::Betting(i) => account
480                    .calculate_initial_margin(i, order.quantity(), price?, None)
481                    .ok()?,
482                InstrumentAny::BinaryOption(i) => account
483                    .calculate_initial_margin(i, order.quantity(), price?, None)
484                    .ok()?,
485                InstrumentAny::Cfd(i) => account
486                    .calculate_initial_margin(i, order.quantity(), price?, None)
487                    .ok()?,
488                InstrumentAny::Commodity(i) => account
489                    .calculate_initial_margin(i, order.quantity(), price?, None)
490                    .ok()?,
491                InstrumentAny::CryptoFuture(i) => account
492                    .calculate_initial_margin(i, order.quantity(), price?, None)
493                    .ok()?,
494                InstrumentAny::CryptoOption(i) => account
495                    .calculate_initial_margin(i, order.quantity(), price?, None)
496                    .ok()?,
497                InstrumentAny::CryptoPerpetual(i) => account
498                    .calculate_initial_margin(i, order.quantity(), price?, None)
499                    .ok()?,
500                InstrumentAny::CurrencyPair(i) => account
501                    .calculate_initial_margin(i, order.quantity(), price?, None)
502                    .ok()?,
503                InstrumentAny::Equity(i) => account
504                    .calculate_initial_margin(i, order.quantity(), price?, None)
505                    .ok()?,
506                InstrumentAny::FuturesContract(i) => account
507                    .calculate_initial_margin(i, order.quantity(), price?, None)
508                    .ok()?,
509                InstrumentAny::FuturesSpread(i) => account
510                    .calculate_initial_margin(i, order.quantity(), price?, None)
511                    .ok()?,
512                InstrumentAny::IndexInstrument(i) => account
513                    .calculate_initial_margin(i, order.quantity(), price?, None)
514                    .ok()?,
515                InstrumentAny::OptionContract(i) => account
516                    .calculate_initial_margin(i, order.quantity(), price?, None)
517                    .ok()?,
518                InstrumentAny::OptionSpread(i) => account
519                    .calculate_initial_margin(i, order.quantity(), price?, None)
520                    .ok()?,
521                InstrumentAny::PerpetualContract(i) => account
522                    .calculate_initial_margin(i, order.quantity(), price?, None)
523                    .ok()?,
524                InstrumentAny::TokenizedAsset(i) => account
525                    .calculate_initial_margin(i, order.quantity(), price?, None)
526                    .ok()?,
527            };
528
529            let mut margin_init = margin_init.as_f64();
530
531            if let Some(base_currency) = account.base_currency {
532                if base_xrate.is_none() {
533                    currency = base_currency;
534                    base_xrate = self
535                        .calculate_xrate_to_base(&AccountAny::Margin(account.clone()), instrument);
536                }
537
538                if let Some(xrate) = base_xrate {
539                    margin_init *= xrate;
540                } else {
541                    log::debug!(
542                        "Cannot calculate initial margin: insufficient data for {}/{}",
543                        instrument.settlement_currency(),
544                        base_currency
545                    );
546                    continue;
547                }
548            }
549
550            total_margin_init += margin_init;
551        }
552
553        let money = Money::new(total_margin_init, currency);
554        let margin_init = {
555            account.update_initial_margin(instrument.id(), money);
556            money
557        };
558
559        log::info!("{} margin_init={margin_init}", instrument.id());
560
561        Some((
562            account.clone(),
563            self.generate_account_state(AccountAny::Margin(account), ts_event),
564        ))
565    }
566
567    fn update_balance_locked_betting(
568        &self,
569        account: &BettingAccount,
570        instrument: &InstrumentAny,
571        orders_open: &[&OrderAny],
572        ts_event: UnixNanos,
573    ) -> Option<(BettingAccount, AccountState)> {
574        let mut account = account.clone();
575
576        if orders_open.is_empty() {
577            account.clear_balance_locked(instrument.id());
578            return Some((
579                account.clone(),
580                self.generate_account_state(AccountAny::Betting(account), ts_event),
581            ));
582        }
583
584        let mut total_locked: AHashMap<Currency, Money> = AHashMap::new();
585        let mut base_xrate: Option<f64> = None;
586        let mut currency = instrument.settlement_currency();
587
588        for order in orders_open {
589            assert_eq!(
590                order.instrument_id(),
591                instrument.id(),
592                "Order not for instrument {}",
593                instrument.id()
594            );
595            assert!(order.is_open(), "Order is not open");
596
597            if order.price().is_none() && order.trigger_price().is_none() {
598                continue;
599            }
600
601            if order.is_reduce_only() {
602                continue;
603            }
604
605            let price = if order.price().is_some() {
606                order.price()
607            } else {
608                order.trigger_price()
609            };
610
611            let mut locked = account
612                .calculate_balance_locked(
613                    instrument,
614                    order.order_side(),
615                    order.quantity(),
616                    price?,
617                    None,
618                )
619                .unwrap();
620
621            if let Some(base_curr) = account.base_currency() {
622                if base_xrate.is_none() {
623                    currency = base_curr;
624                    base_xrate = self
625                        .calculate_xrate_to_base(&AccountAny::Betting(account.clone()), instrument);
626                }
627
628                if let Some(xrate) = base_xrate {
629                    locked = Money::new(locked.as_f64() * xrate, currency);
630                } else {
631                    log::error!(
632                        "Cannot calculate balance locked: insufficient data for {}/{}",
633                        instrument.settlement_currency(),
634                        base_curr
635                    );
636                    return None;
637                }
638            }
639
640            total_locked
641                .entry(locked.currency)
642                .and_modify(|total| *total = *total + locked)
643                .or_insert(locked);
644        }
645
646        if total_locked.is_empty() {
647            account.clear_balance_locked(instrument.id());
648            return Some((
649                account.clone(),
650                self.generate_account_state(AccountAny::Betting(account), ts_event),
651            ));
652        }
653
654        account.clear_balance_locked(instrument.id());
655
656        for (_, balance_locked) in total_locked {
657            account.update_balance_locked(instrument.id(), balance_locked);
658            log::info!("{} balance_locked={balance_locked}", instrument.id());
659        }
660
661        Some((
662            account.clone(),
663            self.generate_account_state(AccountAny::Betting(account), ts_event),
664        ))
665    }
666
667    fn update_balance_single_currency(
668        &self,
669        account: AccountAny,
670        fill: &OrderFilled,
671        mut pnl: Money,
672    ) {
673        let base_currency = if let Some(currency) = account.base_currency() {
674            currency
675        } else {
676            log::error!("Account has no base currency set");
677            return;
678        };
679
680        let mut balances = Vec::new();
681        let mut commission = fill.commission;
682
683        if let Some(ref mut comm) = commission
684            && comm.currency != base_currency
685        {
686            let xrate = self.cache.borrow().get_xrate(
687                fill.instrument_id.venue,
688                comm.currency,
689                base_currency,
690                if fill.order_side == OrderSide::Sell {
691                    PriceType::Bid
692                } else {
693                    PriceType::Ask
694                },
695            );
696
697            if let Some(xrate) = xrate {
698                *comm = Money::new(comm.as_f64() * xrate, base_currency);
699            } else {
700                log::error!(
701                    "Cannot calculate account state: insufficient data for {}/{}",
702                    comm.currency,
703                    base_currency
704                );
705                return;
706            }
707        }
708
709        if pnl.currency != base_currency {
710            let xrate = self.cache.borrow().get_xrate(
711                fill.instrument_id.venue,
712                pnl.currency,
713                base_currency,
714                if fill.order_side == OrderSide::Sell {
715                    PriceType::Bid
716                } else {
717                    PriceType::Ask
718                },
719            );
720
721            if let Some(xrate) = xrate {
722                pnl = Money::new(pnl.as_f64() * xrate, base_currency);
723            } else {
724                log::error!(
725                    "Cannot calculate account state: insufficient data for {}/{}",
726                    pnl.currency,
727                    base_currency
728                );
729                return;
730            }
731        }
732
733        if let Some(comm) = commission {
734            pnl = pnl - comm;
735        }
736
737        if pnl.is_zero() {
738            return;
739        }
740
741        let existing_balances = account.balances();
742        let balance = if let Some(b) = existing_balances.get(&pnl.currency) {
743            b
744        } else {
745            log::error!(
746                "Cannot complete transaction: no balance for {}",
747                pnl.currency
748            );
749            return;
750        };
751
752        let new_balance =
753            AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl);
754        balances.push(new_balance);
755
756        match account {
757            AccountAny::Margin(mut margin) => {
758                margin.update_balances(&balances);
759
760                if let Some(comm) = commission {
761                    margin.update_commissions(comm);
762                }
763            }
764            AccountAny::Cash(mut cash) => {
765                if let Err(e) = cash.update_balances(&balances) {
766                    log::error!("Cannot update cash account balance: {e}");
767                    return;
768                }
769
770                if let Some(comm) = commission {
771                    cash.update_commissions(comm);
772                }
773            }
774            AccountAny::Betting(mut betting) => {
775                if let Err(e) = betting.update_balances(&balances) {
776                    log::error!("Cannot update betting account balance: {e}");
777                    return;
778                }
779
780                if let Some(comm) = commission {
781                    betting.update_commissions(comm);
782                }
783            }
784        }
785    }
786
787    fn update_balance_multi_currency(
788        &self,
789        account: AccountAny,
790        fill: OrderFilled,
791        pnls: &mut [Money],
792    ) {
793        let mut new_balances = Vec::new();
794        let commission = fill.commission;
795        let mut apply_commission = commission.is_some_and(|c| !c.is_zero());
796
797        for pnl in pnls.iter_mut() {
798            if apply_commission && pnl.currency == commission.unwrap().currency {
799                *pnl = *pnl - commission.unwrap();
800                apply_commission = false;
801            }
802
803            if pnl.is_zero() {
804                continue; // No Adjustment
805            }
806
807            let currency = pnl.currency;
808            let balances = account.balances();
809
810            let new_balance = if let Some(balance) = balances.get(&currency) {
811                let new_total = balance.total.as_f64() + pnl.as_f64();
812                let new_free = balance.free.as_f64() + pnl.as_f64();
813                let total = Money::new(new_total, currency);
814                let free = Money::new(new_free, currency);
815
816                if new_total < 0.0 {
817                    log::error!(
818                        "AccountBalanceNegative: balance = {}, currency = {}",
819                        total.as_decimal(),
820                        currency
821                    );
822                    return;
823                }
824
825                if new_free < 0.0 {
826                    log::error!(
827                        "AccountMarginExceeded: balance = {}, margin = {}, currency = {}",
828                        total.as_decimal(),
829                        balance.locked.as_decimal(),
830                        currency
831                    );
832                    return;
833                }
834
835                AccountBalance::new(total, balance.locked, free)
836            } else {
837                if pnl.as_decimal() < Decimal::ZERO {
838                    log::error!(
839                        "Cannot complete transaction: no {currency} to deduct a {pnl} realized PnL from"
840                    );
841                    return;
842                }
843                AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl)
844            };
845
846            new_balances.push(new_balance);
847        }
848
849        if apply_commission {
850            let commission = commission.unwrap();
851            let currency = commission.currency;
852            let balances = account.balances();
853
854            let commission_balance = if let Some(balance) = balances.get(&currency) {
855                let new_total = balance.total.as_decimal() - commission.as_decimal();
856                let new_free = balance.free.as_decimal() - commission.as_decimal();
857                AccountBalance::new(
858                    Money::new(new_total.to_f64().unwrap(), currency),
859                    balance.locked,
860                    Money::new(new_free.to_f64().unwrap(), currency),
861                )
862            } else {
863                if commission.as_decimal() > Decimal::ZERO {
864                    log::error!(
865                        "Cannot complete transaction: no {currency} balance to deduct a {commission} commission from"
866                    );
867                    return;
868                }
869                AccountBalance::new(
870                    Money::new(0.0, currency),
871                    Money::new(0.0, currency),
872                    Money::new(0.0, currency),
873                )
874            };
875            new_balances.push(commission_balance);
876        }
877
878        if new_balances.is_empty() {
879            return;
880        }
881
882        match account {
883            AccountAny::Margin(mut margin) => {
884                margin.update_balances(&new_balances);
885
886                if let Some(commission) = commission {
887                    margin.update_commissions(commission);
888                }
889            }
890            AccountAny::Cash(mut cash) => {
891                if let Err(e) = cash.update_balances(&new_balances) {
892                    log::error!("Cannot update cash account balance: {e}");
893                    return;
894                }
895
896                if let Some(commission) = commission {
897                    cash.update_commissions(commission);
898                }
899            }
900            AccountAny::Betting(mut betting) => {
901                if let Err(e) = betting.update_balances(&new_balances) {
902                    log::error!("Cannot update betting account balance: {e}");
903                    return;
904                }
905
906                if let Some(commission) = commission {
907                    betting.update_commissions(commission);
908                }
909            }
910        }
911    }
912
913    fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState {
914        match account {
915            AccountAny::Margin(margin_account) => {
916                // Include both per-instrument (`margins`) and account-wide
917                // (`account_margins`, keyed by collateral currency) entries so
918                // regenerated state events preserve the full margin picture.
919                let mut margins: Vec<_> = margin_account.margins.values().copied().collect();
920                margins.extend(margin_account.account_margins.values().copied());
921                AccountState::new(
922                    margin_account.id,
923                    AccountType::Margin,
924                    vec![],
925                    margins,
926                    false,
927                    UUID4::new(),
928                    ts_event,
929                    self.clock.borrow().timestamp_ns(),
930                    margin_account.base_currency(),
931                )
932            }
933            AccountAny::Cash(cash_account) => AccountState::new(
934                cash_account.id,
935                AccountType::Cash,
936                cash_account.balances.clone().into_values().collect(),
937                vec![],
938                false,
939                UUID4::new(),
940                ts_event,
941                self.clock.borrow().timestamp_ns(),
942                cash_account.base_currency(),
943            ),
944            AccountAny::Betting(betting_account) => AccountState::new(
945                betting_account.id,
946                AccountType::Betting,
947                betting_account.balances.clone().into_values().collect(),
948                vec![],
949                false,
950                UUID4::new(),
951                ts_event,
952                self.clock.borrow().timestamp_ns(),
953                betting_account.base_currency(),
954            ),
955        }
956    }
957
958    fn calculate_xrate_to_base(
959        &self,
960        account: &AccountAny,
961        instrument: &InstrumentAny,
962    ) -> Option<f64> {
963        match account.base_currency() {
964            None => Some(1.0),
965            Some(base_curr) => self.cache.borrow().get_xrate(
966                instrument.id().venue,
967                instrument.settlement_currency(),
968                base_curr,
969                PriceType::Mid,
970            ),
971        }
972    }
973}
974
975#[cfg(test)]
976mod tests {
977    use std::{cell::RefCell, rc::Rc};
978
979    use nautilus_common::{cache::Cache, clock::TestClock};
980    use nautilus_model::{
981        accounts::{BettingAccount, CashAccount, MarginAccount},
982        enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType},
983        events::{AccountState, OrderAccepted, OrderEventAny, OrderFilled, OrderSubmitted},
984        identifiers::{
985            AccountId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
986        },
987        instruments::{
988            Instrument, InstrumentAny,
989            stubs::{audusd_sim, betting},
990        },
991        orders::{OrderAny, OrderTestBuilder},
992        position::Position,
993        stubs::TestDefault,
994        types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
995    };
996    use rstest::rstest;
997
998    use super::*;
999
1000    #[rstest]
1001    fn test_update_balance_locked_with_base_currency_multiple_orders() {
1002        let usd = Currency::USD();
1003        let account_state = AccountState::new(
1004            AccountId::new("SIM-001"),
1005            AccountType::Cash,
1006            vec![AccountBalance::new(
1007                Money::new(1_000_000.0, usd),
1008                Money::new(0.0, usd),
1009                Money::new(1_000_000.0, usd),
1010            )],
1011            Vec::new(),
1012            true,
1013            UUID4::new(),
1014            UnixNanos::default(),
1015            UnixNanos::default(),
1016            Some(usd),
1017        );
1018
1019        let account = CashAccount::new(account_state, true, false);
1020
1021        let clock = Rc::new(RefCell::new(TestClock::new()));
1022        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1023        cache
1024            .borrow_mut()
1025            .add_account(AccountAny::Cash(account.clone()))
1026            .unwrap();
1027
1028        let manager = AccountsManager::new(clock, cache);
1029
1030        let instrument = audusd_sim();
1031
1032        let order1 = OrderTestBuilder::new(OrderType::Limit)
1033            .instrument_id(instrument.id())
1034            .side(OrderSide::Buy)
1035            .quantity(Quantity::from("100000"))
1036            .price(Price::from("0.75000"))
1037            .build();
1038
1039        let order2 = OrderTestBuilder::new(OrderType::Limit)
1040            .instrument_id(instrument.id())
1041            .side(OrderSide::Buy)
1042            .quantity(Quantity::from("50000"))
1043            .price(Price::from("0.74500"))
1044            .build();
1045
1046        let order3 = OrderTestBuilder::new(OrderType::Limit)
1047            .instrument_id(instrument.id())
1048            .side(OrderSide::Buy)
1049            .quantity(Quantity::from("75000"))
1050            .price(Price::from("0.74000"))
1051            .build();
1052
1053        let mut order1 = order1;
1054        let mut order2 = order2;
1055        let mut order3 = order3;
1056
1057        let submitted1 = OrderSubmitted::new(
1058            order1.trader_id(),
1059            order1.strategy_id(),
1060            order1.instrument_id(),
1061            order1.client_order_id(),
1062            AccountId::new("SIM-001"),
1063            UUID4::new(),
1064            UnixNanos::default(),
1065            UnixNanos::default(),
1066        );
1067
1068        let accepted1 = OrderAccepted::new(
1069            order1.trader_id(),
1070            order1.strategy_id(),
1071            order1.instrument_id(),
1072            order1.client_order_id(),
1073            order1.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1074            AccountId::new("SIM-001"),
1075            UUID4::new(),
1076            UnixNanos::default(),
1077            UnixNanos::default(),
1078            false,
1079        );
1080
1081        order1.apply(OrderEventAny::Submitted(submitted1)).unwrap();
1082        order1.apply(OrderEventAny::Accepted(accepted1)).unwrap();
1083
1084        let submitted2 = OrderSubmitted::new(
1085            order2.trader_id(),
1086            order2.strategy_id(),
1087            order2.instrument_id(),
1088            order2.client_order_id(),
1089            AccountId::new("SIM-001"),
1090            UUID4::new(),
1091            UnixNanos::default(),
1092            UnixNanos::default(),
1093        );
1094
1095        let accepted2 = OrderAccepted::new(
1096            order2.trader_id(),
1097            order2.strategy_id(),
1098            order2.instrument_id(),
1099            order2.client_order_id(),
1100            order2.venue_order_id().unwrap_or(VenueOrderId::new("2")),
1101            AccountId::new("SIM-001"),
1102            UUID4::new(),
1103            UnixNanos::default(),
1104            UnixNanos::default(),
1105            false,
1106        );
1107
1108        order2.apply(OrderEventAny::Submitted(submitted2)).unwrap();
1109        order2.apply(OrderEventAny::Accepted(accepted2)).unwrap();
1110
1111        let submitted3 = OrderSubmitted::new(
1112            order3.trader_id(),
1113            order3.strategy_id(),
1114            order3.instrument_id(),
1115            order3.client_order_id(),
1116            AccountId::new("SIM-001"),
1117            UUID4::new(),
1118            UnixNanos::default(),
1119            UnixNanos::default(),
1120        );
1121
1122        let accepted3 = OrderAccepted::new(
1123            order3.trader_id(),
1124            order3.strategy_id(),
1125            order3.instrument_id(),
1126            order3.client_order_id(),
1127            order3.venue_order_id().unwrap_or(VenueOrderId::new("3")),
1128            AccountId::new("SIM-001"),
1129            UUID4::new(),
1130            UnixNanos::default(),
1131            UnixNanos::default(),
1132            false,
1133        );
1134
1135        order3.apply(OrderEventAny::Submitted(submitted3)).unwrap();
1136        order3.apply(OrderEventAny::Accepted(accepted3)).unwrap();
1137
1138        let orders: Vec<&OrderAny> = vec![&order1, &order2, &order3];
1139
1140        let result = manager.update_orders(
1141            &AccountAny::Cash(account),
1142            &InstrumentAny::CurrencyPair(instrument),
1143            orders,
1144            UnixNanos::default(),
1145        );
1146
1147        assert!(result.is_some());
1148        let (updated_account, _state) = result.unwrap();
1149
1150        if let AccountAny::Cash(cash_account) = updated_account {
1151            let locked_balance = cash_account.balance_locked(Some(usd));
1152
1153            // Order 1: 100k * 0.75 = 75k, Order 2: 50k * 0.745 = 37.25k, Order 3: 75k * 0.74 = 55.5k
1154            let expected_locked = Money::new(167_750.0, usd);
1155
1156            assert_eq!(locked_balance, Some(expected_locked));
1157            let aud = Currency::AUD();
1158            assert_eq!(cash_account.balance_locked(Some(aud)), None);
1159        } else {
1160            panic!("Expected CashAccount");
1161        }
1162    }
1163
1164    #[rstest]
1165    fn test_update_orders_betting_account_uses_liability_for_locked_balance() {
1166        let gbp = Currency::GBP();
1167        let account_state = AccountState::new(
1168            AccountId::new("BETTING-001"),
1169            AccountType::Betting,
1170            vec![AccountBalance::new(
1171                Money::new(1_000.0, gbp),
1172                Money::new(0.0, gbp),
1173                Money::new(1_000.0, gbp),
1174            )],
1175            Vec::new(),
1176            true,
1177            UUID4::new(),
1178            UnixNanos::default(),
1179            UnixNanos::default(),
1180            Some(gbp),
1181        );
1182
1183        let account = BettingAccount::new(account_state, true);
1184
1185        let clock = Rc::new(RefCell::new(TestClock::new()));
1186        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1187        cache
1188            .borrow_mut()
1189            .add_account(AccountAny::Betting(account.clone()))
1190            .unwrap();
1191
1192        let manager = AccountsManager::new(clock, cache);
1193        let instrument = betting();
1194
1195        let mut back_order = OrderTestBuilder::new(OrderType::Limit)
1196            .instrument_id(instrument.id())
1197            .side(OrderSide::Buy)
1198            .quantity(Quantity::from("10"))
1199            .price(Price::from("1.25"))
1200            .build();
1201
1202        let mut lay_order = OrderTestBuilder::new(OrderType::Limit)
1203            .instrument_id(instrument.id())
1204            .side(OrderSide::Sell)
1205            .quantity(Quantity::from("12"))
1206            .price(Price::from("3.00"))
1207            .build();
1208
1209        let submitted_back = OrderSubmitted::new(
1210            back_order.trader_id(),
1211            back_order.strategy_id(),
1212            back_order.instrument_id(),
1213            back_order.client_order_id(),
1214            AccountId::new("BETTING-001"),
1215            UUID4::new(),
1216            UnixNanos::default(),
1217            UnixNanos::default(),
1218        );
1219        let accepted_back = OrderAccepted::new(
1220            back_order.trader_id(),
1221            back_order.strategy_id(),
1222            back_order.instrument_id(),
1223            back_order.client_order_id(),
1224            VenueOrderId::new("B1"),
1225            AccountId::new("BETTING-001"),
1226            UUID4::new(),
1227            UnixNanos::default(),
1228            UnixNanos::default(),
1229            false,
1230        );
1231        back_order
1232            .apply(OrderEventAny::Submitted(submitted_back))
1233            .unwrap();
1234        back_order
1235            .apply(OrderEventAny::Accepted(accepted_back))
1236            .unwrap();
1237
1238        let submitted_lay = OrderSubmitted::new(
1239            lay_order.trader_id(),
1240            lay_order.strategy_id(),
1241            lay_order.instrument_id(),
1242            lay_order.client_order_id(),
1243            AccountId::new("BETTING-001"),
1244            UUID4::new(),
1245            UnixNanos::default(),
1246            UnixNanos::default(),
1247        );
1248        let accepted_lay = OrderAccepted::new(
1249            lay_order.trader_id(),
1250            lay_order.strategy_id(),
1251            lay_order.instrument_id(),
1252            lay_order.client_order_id(),
1253            VenueOrderId::new("L1"),
1254            AccountId::new("BETTING-001"),
1255            UUID4::new(),
1256            UnixNanos::default(),
1257            UnixNanos::default(),
1258            false,
1259        );
1260        lay_order
1261            .apply(OrderEventAny::Submitted(submitted_lay))
1262            .unwrap();
1263        lay_order
1264            .apply(OrderEventAny::Accepted(accepted_lay))
1265            .unwrap();
1266
1267        let orders: Vec<&OrderAny> = vec![&back_order, &lay_order];
1268        let result = manager.update_orders(
1269            &AccountAny::Betting(account),
1270            &InstrumentAny::Betting(instrument),
1271            orders,
1272            UnixNanos::default(),
1273        );
1274
1275        assert!(result.is_some());
1276        let (updated_account, state) = result.unwrap();
1277
1278        if let AccountAny::Betting(betting_account) = updated_account {
1279            assert_eq!(
1280                betting_account.balance_locked(Some(gbp)),
1281                Some(Money::new(14.5, gbp))
1282            );
1283            assert_eq!(
1284                betting_account.balance_free(Some(gbp)),
1285                Some(Money::new(985.5, gbp))
1286            );
1287            assert_eq!(state.account_type, AccountType::Betting);
1288        } else {
1289            panic!("Expected BettingAccount");
1290        }
1291    }
1292
1293    #[rstest]
1294    fn test_betting_order_canceled_releases_locked_balance() {
1295        let gbp = Currency::GBP();
1296        let account_state = AccountState::new(
1297            AccountId::new("BETFAIR-001"),
1298            AccountType::Betting,
1299            vec![AccountBalance::new(
1300                Money::new(1_000.0, gbp),
1301                Money::new(0.0, gbp),
1302                Money::new(1_000.0, gbp),
1303            )],
1304            Vec::new(),
1305            true,
1306            UUID4::new(),
1307            UnixNanos::default(),
1308            UnixNanos::default(),
1309            Some(gbp),
1310        );
1311
1312        let account = BettingAccount::new(account_state, true);
1313
1314        let clock = Rc::new(RefCell::new(TestClock::new()));
1315        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1316        cache
1317            .borrow_mut()
1318            .add_account(AccountAny::Betting(account.clone()))
1319            .unwrap();
1320
1321        let manager = AccountsManager::new(clock, cache);
1322        let instrument = betting();
1323
1324        let mut order = OrderTestBuilder::new(OrderType::Limit)
1325            .instrument_id(instrument.id())
1326            .side(OrderSide::Buy)
1327            .quantity(Quantity::from("10"))
1328            .price(Price::from("5.0"))
1329            .build();
1330
1331        let submitted = OrderSubmitted::new(
1332            order.trader_id(),
1333            order.strategy_id(),
1334            order.instrument_id(),
1335            order.client_order_id(),
1336            AccountId::new("BETFAIR-001"),
1337            UUID4::new(),
1338            UnixNanos::default(),
1339            UnixNanos::default(),
1340        );
1341        let accepted = OrderAccepted::new(
1342            order.trader_id(),
1343            order.strategy_id(),
1344            order.instrument_id(),
1345            order.client_order_id(),
1346            VenueOrderId::new("B2"),
1347            AccountId::new("BETFAIR-001"),
1348            UUID4::new(),
1349            UnixNanos::default(),
1350            UnixNanos::default(),
1351            false,
1352        );
1353
1354        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1355        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1356
1357        let result = manager.update_orders(
1358            &AccountAny::Betting(account),
1359            &InstrumentAny::Betting(instrument.clone()),
1360            vec![&order],
1361            UnixNanos::default(),
1362        );
1363
1364        assert!(result.is_some());
1365        let (updated_account, _) = result.unwrap();
1366
1367        if let AccountAny::Betting(ref betting_account) = updated_account {
1368            assert_eq!(
1369                betting_account.balance_locked(Some(gbp)),
1370                Some(Money::new(40.0, gbp))
1371            );
1372            assert_eq!(
1373                betting_account.balance_free(Some(gbp)),
1374                Some(Money::new(960.0, gbp))
1375            );
1376        } else {
1377            panic!("Expected BettingAccount");
1378        }
1379
1380        let result = manager.update_orders(
1381            &updated_account,
1382            &InstrumentAny::Betting(instrument),
1383            vec![],
1384            UnixNanos::default(),
1385        );
1386
1387        assert!(result.is_some());
1388        let (final_account, _) = result.unwrap();
1389
1390        if let AccountAny::Betting(betting_account) = final_account {
1391            assert_eq!(
1392                betting_account.balance_locked(Some(gbp)),
1393                Some(Money::new(0.0, gbp))
1394            );
1395            assert_eq!(
1396                betting_account.balance_free(Some(gbp)),
1397                Some(Money::new(1_000.0, gbp))
1398            );
1399            assert_eq!(
1400                betting_account.balance_total(Some(gbp)),
1401                Some(Money::new(1_000.0, gbp))
1402            );
1403        } else {
1404            panic!("Expected BettingAccount");
1405        }
1406    }
1407
1408    #[rstest]
1409    fn test_update_orders_clears_stale_currency_locks_when_order_sides_change() {
1410        let usd = Currency::USD();
1411        let aud = Currency::AUD();
1412        let account_state = AccountState::new(
1413            AccountId::new("SIM-001"),
1414            AccountType::Cash,
1415            vec![
1416                AccountBalance::new(
1417                    Money::new(1_000_000.0, usd),
1418                    Money::new(0.0, usd),
1419                    Money::new(1_000_000.0, usd),
1420                ),
1421                AccountBalance::new(
1422                    Money::new(1_000_000.0, aud),
1423                    Money::new(0.0, aud),
1424                    Money::new(1_000_000.0, aud),
1425                ),
1426            ],
1427            Vec::new(),
1428            true,
1429            UUID4::new(),
1430            UnixNanos::default(),
1431            UnixNanos::default(),
1432            None,
1433        );
1434
1435        let account = CashAccount::new(account_state, true, false);
1436
1437        let clock = Rc::new(RefCell::new(TestClock::new()));
1438        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1439        cache
1440            .borrow_mut()
1441            .add_account(AccountAny::Cash(account.clone()))
1442            .unwrap();
1443
1444        let manager = AccountsManager::new(clock, cache);
1445        let instrument = audusd_sim();
1446
1447        let mut buy_order = OrderTestBuilder::new(OrderType::Limit)
1448            .instrument_id(instrument.id())
1449            .side(OrderSide::Buy)
1450            .quantity(Quantity::from("100000"))
1451            .price(Price::from("0.80000"))
1452            .build();
1453
1454        let mut sell_order = OrderTestBuilder::new(OrderType::Limit)
1455            .instrument_id(instrument.id())
1456            .side(OrderSide::Sell)
1457            .quantity(Quantity::from("50000"))
1458            .price(Price::from("0.81000"))
1459            .build();
1460
1461        // Submit and accept orders
1462        let submitted_buy = OrderSubmitted::new(
1463            buy_order.trader_id(),
1464            buy_order.strategy_id(),
1465            buy_order.instrument_id(),
1466            buy_order.client_order_id(),
1467            AccountId::new("SIM-001"),
1468            UUID4::new(),
1469            UnixNanos::default(),
1470            UnixNanos::default(),
1471        );
1472        let accepted_buy = OrderAccepted::new(
1473            buy_order.trader_id(),
1474            buy_order.strategy_id(),
1475            buy_order.instrument_id(),
1476            buy_order.client_order_id(),
1477            VenueOrderId::new("1"),
1478            AccountId::new("SIM-001"),
1479            UUID4::new(),
1480            UnixNanos::default(),
1481            UnixNanos::default(),
1482            false,
1483        );
1484        buy_order
1485            .apply(OrderEventAny::Submitted(submitted_buy))
1486            .unwrap();
1487        buy_order
1488            .apply(OrderEventAny::Accepted(accepted_buy))
1489            .unwrap();
1490
1491        let submitted_sell = OrderSubmitted::new(
1492            sell_order.trader_id(),
1493            sell_order.strategy_id(),
1494            sell_order.instrument_id(),
1495            sell_order.client_order_id(),
1496            AccountId::new("SIM-001"),
1497            UUID4::new(),
1498            UnixNanos::default(),
1499            UnixNanos::default(),
1500        );
1501        let accepted_sell = OrderAccepted::new(
1502            sell_order.trader_id(),
1503            sell_order.strategy_id(),
1504            sell_order.instrument_id(),
1505            sell_order.client_order_id(),
1506            VenueOrderId::new("2"),
1507            AccountId::new("SIM-001"),
1508            UUID4::new(),
1509            UnixNanos::default(),
1510            UnixNanos::default(),
1511            false,
1512        );
1513        sell_order
1514            .apply(OrderEventAny::Submitted(submitted_sell))
1515            .unwrap();
1516        sell_order
1517            .apply(OrderEventAny::Accepted(accepted_sell))
1518            .unwrap();
1519
1520        let orders_both: Vec<&OrderAny> = vec![&buy_order, &sell_order];
1521        let result = manager.update_orders(
1522            &AccountAny::Cash(account),
1523            &InstrumentAny::CurrencyPair(instrument.clone()),
1524            orders_both,
1525            UnixNanos::default(),
1526        );
1527
1528        assert!(result.is_some());
1529        let (updated_account, _) = result.unwrap();
1530
1531        if let AccountAny::Cash(cash_account) = &updated_account {
1532            assert_eq!(
1533                cash_account.balance_locked(Some(usd)),
1534                Some(Money::new(80_000.0, usd))
1535            );
1536            assert_eq!(
1537                cash_account.balance_locked(Some(aud)),
1538                Some(Money::new(50_000.0, aud))
1539            );
1540        } else {
1541            panic!("Expected CashAccount");
1542        }
1543
1544        // Cancel BUY order, only SELL remains - USD lock should be cleared
1545        let orders_sell_only: Vec<&OrderAny> = vec![&sell_order];
1546        let result = manager.update_orders(
1547            &updated_account,
1548            &InstrumentAny::CurrencyPair(instrument),
1549            orders_sell_only,
1550            UnixNanos::default(),
1551        );
1552
1553        assert!(result.is_some());
1554        let (final_account, _) = result.unwrap();
1555
1556        if let AccountAny::Cash(cash_account) = final_account {
1557            assert_eq!(
1558                cash_account.balance_locked(Some(usd)),
1559                Some(Money::new(0.0, usd))
1560            );
1561            assert_eq!(
1562                cash_account.balance_locked(Some(aud)),
1563                Some(Money::new(50_000.0, aud))
1564            );
1565        } else {
1566            panic!("Expected CashAccount");
1567        }
1568    }
1569
1570    #[rstest]
1571    fn test_cash_account_rejects_negative_balance_when_borrowing_disabled() {
1572        let usd = Currency::USD();
1573        let account_state = AccountState::new(
1574            AccountId::new("SIM-001"),
1575            AccountType::Cash,
1576            vec![AccountBalance::new(
1577                Money::new(1_000.0, usd),
1578                Money::new(0.0, usd),
1579                Money::new(1_000.0, usd),
1580            )],
1581            Vec::new(),
1582            true,
1583            UUID4::new(),
1584            UnixNanos::default(),
1585            UnixNanos::default(),
1586            Some(usd),
1587        );
1588
1589        let mut account = CashAccount::new(account_state, true, false);
1590
1591        let negative_balances = vec![AccountBalance::new(
1592            Money::new(-500.0, usd),
1593            Money::new(0.0, usd),
1594            Money::new(-500.0, usd),
1595        )];
1596
1597        let result = account.update_balances(&negative_balances);
1598
1599        assert!(result.is_err());
1600        let err_msg = result.unwrap_err().to_string();
1601        assert!(err_msg.contains("negative"));
1602        assert!(err_msg.contains("borrowing not allowed"));
1603    }
1604
1605    #[rstest]
1606    fn test_manager_update_balances_skips_update_on_negative_balance_error() {
1607        let usd = Currency::USD();
1608        let account_state = AccountState::new(
1609            AccountId::new("SIM-001"),
1610            AccountType::Cash,
1611            vec![AccountBalance::new(
1612                Money::new(100.0, usd),
1613                Money::new(0.0, usd),
1614                Money::new(100.0, usd),
1615            )],
1616            Vec::new(),
1617            true,
1618            UUID4::new(),
1619            UnixNanos::default(),
1620            UnixNanos::default(),
1621            Some(usd),
1622        );
1623
1624        let account = CashAccount::new(account_state, true, false);
1625        let initial_balance = account.balance_total(Some(usd)).unwrap();
1626
1627        let clock = Rc::new(RefCell::new(TestClock::new()));
1628        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1629        cache
1630            .borrow_mut()
1631            .add_account(AccountAny::Cash(account.clone()))
1632            .unwrap();
1633
1634        let manager = AccountsManager::new(clock, cache.clone());
1635        let instrument = audusd_sim();
1636
1637        let mut order = OrderTestBuilder::new(OrderType::Market)
1638            .instrument_id(instrument.id())
1639            .side(OrderSide::Buy)
1640            .quantity(Quantity::from("100000"))
1641            .build();
1642
1643        let submitted = OrderSubmitted::new(
1644            order.trader_id(),
1645            order.strategy_id(),
1646            order.instrument_id(),
1647            order.client_order_id(),
1648            AccountId::new("SIM-001"),
1649            UUID4::new(),
1650            UnixNanos::default(),
1651            UnixNanos::default(),
1652        );
1653        let accepted = OrderAccepted::new(
1654            order.trader_id(),
1655            order.strategy_id(),
1656            order.instrument_id(),
1657            order.client_order_id(),
1658            VenueOrderId::new("1"),
1659            AccountId::new("SIM-001"),
1660            UUID4::new(),
1661            UnixNanos::default(),
1662            UnixNanos::default(),
1663            false,
1664        );
1665        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1666        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1667
1668        cache
1669            .borrow_mut()
1670            .add_order(order.clone(), None, None, false)
1671            .unwrap();
1672
1673        // Fill with large cost ($80k) that exceeds $100 balance
1674        let fill = OrderFilled::new(
1675            TraderId::test_default(),
1676            StrategyId::test_default(),
1677            instrument.id(),
1678            order.client_order_id(),
1679            VenueOrderId::new("1"),
1680            AccountId::new("SIM-001"),
1681            TradeId::new("1"),
1682            OrderSide::Buy,
1683            order.order_type(),
1684            Quantity::from("100000"),
1685            Price::from("0.80000"),
1686            usd,
1687            LiquiditySide::Taker,
1688            UUID4::new(),
1689            UnixNanos::from(1),
1690            UnixNanos::from(1),
1691            false,
1692            Some(PositionId::new("P-001")),
1693            Some(Money::new(20.0, usd)),
1694        );
1695
1696        let position = Position::new(&InstrumentAny::CurrencyPair(instrument.clone()), fill);
1697        cache
1698            .borrow_mut()
1699            .add_position(&position, OmsType::Netting)
1700            .unwrap();
1701
1702        let fill2 = OrderFilled::new(
1703            TraderId::test_default(),
1704            StrategyId::test_default(),
1705            instrument.id(),
1706            order.client_order_id(),
1707            VenueOrderId::new("2"),
1708            AccountId::new("SIM-001"),
1709            TradeId::new("2"),
1710            OrderSide::Buy,
1711            order.order_type(),
1712            Quantity::from("100000"),
1713            Price::from("0.80000"),
1714            usd,
1715            LiquiditySide::Taker,
1716            UUID4::new(),
1717            UnixNanos::from(2),
1718            UnixNanos::from(2),
1719            false,
1720            Some(PositionId::new("P-001")),
1721            Some(Money::new(20.0, usd)),
1722        );
1723        let _state = manager.update_balances(
1724            AccountAny::Cash(account),
1725            &InstrumentAny::CurrencyPair(instrument),
1726            fill2,
1727        );
1728
1729        let account_after = cache
1730            .borrow()
1731            .account(&AccountId::new("SIM-001"))
1732            .unwrap()
1733            .clone();
1734
1735        if let AccountAny::Cash(cash) = account_after {
1736            assert_eq!(cash.balance_total(Some(usd)), Some(initial_balance));
1737        } else {
1738            panic!("Expected CashAccount");
1739        }
1740    }
1741
1742    #[rstest]
1743    fn test_order_canceled_releases_locked_balance() {
1744        // Regression test for https://github.com/nautechsystems/nautilus_trader/issues/3525
1745        let usd = Currency::USD();
1746        let account_state = AccountState::new(
1747            AccountId::new("SIM-001"),
1748            AccountType::Cash,
1749            vec![AccountBalance::new(
1750                Money::new(100_000.0, usd),
1751                Money::new(0.0, usd),
1752                Money::new(100_000.0, usd),
1753            )],
1754            Vec::new(),
1755            true,
1756            UUID4::new(),
1757            UnixNanos::default(),
1758            UnixNanos::default(),
1759            Some(usd),
1760        );
1761
1762        let account = CashAccount::new(account_state, true, false);
1763
1764        let clock = Rc::new(RefCell::new(TestClock::new()));
1765        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1766        cache
1767            .borrow_mut()
1768            .add_account(AccountAny::Cash(account.clone()))
1769            .unwrap();
1770
1771        let manager = AccountsManager::new(clock, cache);
1772        let instrument = audusd_sim();
1773
1774        let mut order = OrderTestBuilder::new(OrderType::Limit)
1775            .instrument_id(instrument.id())
1776            .side(OrderSide::Buy)
1777            .quantity(Quantity::from("100000"))
1778            .price(Price::from("0.80000"))
1779            .build();
1780
1781        let submitted = OrderSubmitted::new(
1782            order.trader_id(),
1783            order.strategy_id(),
1784            order.instrument_id(),
1785            order.client_order_id(),
1786            AccountId::new("SIM-001"),
1787            UUID4::new(),
1788            UnixNanos::default(),
1789            UnixNanos::default(),
1790        );
1791
1792        let accepted = OrderAccepted::new(
1793            order.trader_id(),
1794            order.strategy_id(),
1795            order.instrument_id(),
1796            order.client_order_id(),
1797            order.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1798            AccountId::new("SIM-001"),
1799            UUID4::new(),
1800            UnixNanos::default(),
1801            UnixNanos::default(),
1802            false,
1803        );
1804
1805        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1806        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1807
1808        let result = manager.update_orders(
1809            &AccountAny::Cash(account),
1810            &InstrumentAny::CurrencyPair(instrument.clone()),
1811            vec![&order],
1812            UnixNanos::default(),
1813        );
1814
1815        assert!(result.is_some());
1816        let (updated_account, _) = result.unwrap();
1817
1818        if let AccountAny::Cash(ref cash) = updated_account {
1819            // 100k * 0.80 = 80k USD locked
1820            assert_eq!(
1821                cash.balance_locked(Some(usd)),
1822                Some(Money::new(80_000.0, usd))
1823            );
1824            assert_eq!(
1825                cash.balance_free(Some(usd)),
1826                Some(Money::new(20_000.0, usd))
1827            );
1828        } else {
1829            panic!("Expected CashAccount");
1830        }
1831
1832        let result = manager.update_orders(
1833            &updated_account,
1834            &InstrumentAny::CurrencyPair(instrument),
1835            vec![],
1836            UnixNanos::default(),
1837        );
1838
1839        assert!(result.is_some());
1840        let (final_account, _) = result.unwrap();
1841
1842        if let AccountAny::Cash(cash) = final_account {
1843            assert_eq!(cash.balance_locked(Some(usd)), Some(Money::new(0.0, usd)));
1844            assert_eq!(
1845                cash.balance_free(Some(usd)),
1846                Some(Money::new(100_000.0, usd))
1847            );
1848            assert_eq!(
1849                cash.balance_total(Some(usd)),
1850                Some(Money::new(100_000.0, usd))
1851            );
1852        } else {
1853            panic!("Expected CashAccount");
1854        }
1855    }
1856
1857    #[rstest]
1858    fn test_generate_account_state_preserves_per_instrument_and_account_wide_margins() {
1859        let usd = Currency::USD();
1860        let audusd = InstrumentId::from("AUD/USD.SIM");
1861        let account_state = AccountState::new(
1862            AccountId::new("SIM-001"),
1863            AccountType::Margin,
1864            vec![AccountBalance::new(
1865                Money::new(1_000_000.0, usd),
1866                Money::new(0.0, usd),
1867                Money::new(1_000_000.0, usd),
1868            )],
1869            Vec::new(),
1870            true,
1871            UUID4::new(),
1872            UnixNanos::default(),
1873            UnixNanos::default(),
1874            Some(usd),
1875        );
1876        let mut account = MarginAccount::new(account_state, false);
1877        account.update_margin(MarginBalance::new(
1878            Money::new(150.0, usd),
1879            Money::new(75.0, usd),
1880            Some(audusd),
1881        ));
1882        account.update_margin(MarginBalance::new(
1883            Money::new(500.0, usd),
1884            Money::new(250.0, usd),
1885            None,
1886        ));
1887
1888        let clock = Rc::new(RefCell::new(TestClock::new()));
1889        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1890        let manager = AccountsManager::new(clock, cache);
1891
1892        let state =
1893            manager.generate_account_state(AccountAny::Margin(account), UnixNanos::default());
1894
1895        assert_eq!(state.margins.len(), 2);
1896        let per_instrument: Vec<_> = state
1897            .margins
1898            .iter()
1899            .filter(|m| m.instrument_id.is_some())
1900            .collect();
1901        let account_wide: Vec<_> = state
1902            .margins
1903            .iter()
1904            .filter(|m| m.instrument_id.is_none())
1905            .collect();
1906        assert_eq!(per_instrument.len(), 1);
1907        assert_eq!(per_instrument[0].instrument_id, Some(audusd));
1908        assert_eq!(per_instrument[0].initial, Money::new(150.0, usd));
1909        assert_eq!(per_instrument[0].maintenance, Money::new(75.0, usd));
1910        assert_eq!(account_wide.len(), 1);
1911        assert_eq!(account_wide[0].currency, usd);
1912        assert_eq!(account_wide[0].initial, Money::new(500.0, usd));
1913        assert_eq!(account_wide[0].maintenance, Money::new(250.0, usd));
1914    }
1915}