1use ahash::AHashMap;
22use indexmap::IndexMap;
23use nautilus_core::{
24 DurationNanos, UnixNanos,
25 correctness::{
26 CorrectnessError, CorrectnessResult, FAILED, check_equal, check_predicate_false,
27 check_predicate_true,
28 },
29};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Serialize};
32
33use crate::{
34 enums::{AccountType, LiquiditySide, OrderSide},
35 events::{AccountState, OrderFilled},
36 identifiers::{AccountId, InstrumentId},
37 instruments::{Instrument, InstrumentAny},
38 position::Position,
39 types::{AccountBalance, Currency, Money, Price, Quantity},
40};
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
47)]
48pub struct BaseAccount {
49 pub id: AccountId,
51 pub account_type: AccountType,
53 pub base_currency: Option<Currency>,
55 pub calculate_account_state: bool,
58 pub events: Vec<AccountState>,
60 pub commissions: AHashMap<Currency, Money>,
62 pub balances: IndexMap<Currency, AccountBalance>,
64 pub balances_starting: IndexMap<Currency, Money>,
66}
67
68impl BaseAccount {
69 #[must_use]
71 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
72 let mut balances_starting: IndexMap<Currency, Money> = IndexMap::new();
73 let mut balances: IndexMap<Currency, AccountBalance> = IndexMap::new();
74 event.balances.iter().for_each(|balance| {
75 balances_starting.insert(balance.currency, balance.total);
76 balances.insert(balance.currency, *balance);
77 });
78 Self {
79 id: event.account_id,
80 account_type: event.account_type,
81 base_currency: event.base_currency,
82 calculate_account_state,
83 events: vec![event],
84 commissions: AHashMap::new(),
85 balances,
86 balances_starting,
87 }
88 }
89
90 #[must_use]
91 pub(crate) fn clone_without_events(&self) -> Self {
92 Self {
93 id: self.id,
94 account_type: self.account_type,
95 base_currency: self.base_currency,
96 calculate_account_state: self.calculate_account_state,
97 events: Vec::new(),
98 commissions: self.commissions.clone(),
99 balances: self.balances.clone(),
100 balances_starting: self.balances_starting.clone(),
101 }
102 }
103
104 #[must_use]
110 pub fn base_balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
111 let currency = currency
112 .or(self.base_currency)
113 .expect("Currency must be specified");
114 self.balances.get(¤cy)
115 }
116
117 #[must_use]
123 pub fn base_balance_total(&self, currency: Option<Currency>) -> Option<Money> {
124 self.base_balance(currency).map(|balance| balance.total)
125 }
126
127 #[must_use]
128 pub fn base_balances_total(&self) -> IndexMap<Currency, Money> {
129 self.balances
130 .iter()
131 .map(|(currency, balance)| (*currency, balance.total))
132 .collect()
133 }
134
135 #[must_use]
141 pub fn base_balance_free(&self, currency: Option<Currency>) -> Option<Money> {
142 self.base_balance(currency).map(|balance| balance.free)
143 }
144
145 #[must_use]
146 pub fn base_balances_free(&self) -> IndexMap<Currency, Money> {
147 self.balances
148 .iter()
149 .map(|(currency, balance)| (*currency, balance.free))
150 .collect()
151 }
152
153 #[must_use]
159 pub fn base_balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
160 self.base_balance(currency).map(|balance| balance.locked)
161 }
162
163 #[must_use]
164 pub fn base_balances_locked(&self) -> IndexMap<Currency, Money> {
165 self.balances
166 .iter()
167 .map(|(currency, balance)| (*currency, balance.locked))
168 .collect()
169 }
170
171 #[must_use]
172 pub fn base_last_event(&self) -> Option<AccountState> {
173 self.events.last().cloned()
174 }
175
176 pub fn update_balances(&mut self, balances: &[AccountBalance]) {
183 for balance in balances {
184 self.balances.insert(balance.currency, *balance);
185 }
186 }
187
188 pub fn update_commissions(&mut self, commission: Money) {
195 self.try_update_commissions(commission)
196 .expect("commission total exceeded Money bounds");
197 }
198
199 pub fn try_update_commissions(&mut self, commission: Money) -> anyhow::Result<()> {
205 let commission = commission.normalized();
207 if commission.is_zero() {
208 return Ok(());
209 }
210 let currency = commission.currency;
211 let total = self
212 .commissions
213 .get(¤cy)
214 .copied()
215 .map_or(Some(commission), |total| total.checked_add(commission))
216 .ok_or_else(|| anyhow::anyhow!("{currency} commission total exceeds Money bounds"))?;
217 self.commissions.insert(currency, total);
218 Ok(())
219 }
220
221 #[must_use]
223 pub fn commission(&self, currency: &Currency) -> Option<Money> {
224 self.commissions.get(currency).copied()
225 }
226
227 #[must_use]
229 pub fn commissions(&self) -> AHashMap<Currency, Money> {
230 self.commissions.clone()
231 }
232
233 pub(crate) fn check_event_account_id(&self, event: &AccountState) -> anyhow::Result<()> {
242 anyhow::ensure!(
243 event.account_id == self.id,
244 "Account event had a different account ID: expected {}, received {}",
245 self.id,
246 event.account_id
247 );
248 Ok(())
249 }
250
251 pub fn base_apply(&mut self, event: AccountState) {
258 check_equal(&event.account_id, &self.id, "event.account_id", "self.id").expect(FAILED);
259 self.update_balances(&event.balances);
260 self.events.push(event);
261 }
262
263 pub fn base_purge_account_events(&mut self, ts_now: UnixNanos, lookback_secs: u64) {
271 let Ok(lookback_ns) = DurationNanos::try_from_secs(lookback_secs) else {
272 log::warn!(
273 "Cannot purge account events: lookback_secs {lookback_secs} is not representable in `u64` nanoseconds"
274 );
275 return;
276 };
277 let purge_cutoff = ts_now.checked_sub(lookback_ns);
278
279 let mut retained_events = Vec::new();
280
281 for event in &self.events {
282 if purge_cutoff.is_none_or(|cutoff| event.ts_event > cutoff) {
283 retained_events.push(event.clone());
284 }
285 }
286
287 if retained_events.is_empty() && !self.events.is_empty() {
289 retained_events.push(self.events.last().expect("events not empty").clone());
290 }
291
292 self.events = retained_events;
293 }
294
295 pub fn base_calculate_balance_locked(
301 &self,
302 instrument: &InstrumentAny,
303 side: OrderSide,
304 quantity: Quantity,
305 price: Price,
306 use_quote_for_inverse: Option<bool>,
307 ) -> anyhow::Result<Money> {
308 let base_currency = instrument
309 .base_currency()
310 .unwrap_or(instrument.quote_currency());
311 let quote_currency = instrument.quote_currency();
312 let amount = match side {
313 OrderSide::Buy => instrument
317 .try_calculate_notional_value(quantity, price, use_quote_for_inverse)?
318 .as_decimal()
319 .max(Decimal::ZERO),
320 OrderSide::Sell => quantity.as_decimal(),
321 };
322
323 if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) {
324 Ok(Money::from_decimal(amount, base_currency)?)
325 } else {
326 let currency = match side {
327 OrderSide::Buy => quote_currency,
328 OrderSide::Sell => base_currency,
329 };
330 Ok(Money::from_decimal(amount, currency)?)
331 }
332 }
333
334 pub fn base_calculate_pnls(
347 &self,
348 instrument: &InstrumentAny,
349 fill: &OrderFilled,
350 _position: Option<Position>,
351 ) -> anyhow::Result<Vec<Money>> {
352 let mut pnls: IndexMap<Currency, Money> = IndexMap::new();
353 let base_currency = instrument.base_currency();
354
355 let fill_qty = fill.last_qty;
357 let notional = instrument.try_calculate_notional_value(fill_qty, fill.last_px, None)?;
358
359 if fill.order_side == OrderSide::Buy {
360 if let (Some(base_currency_value), None) = (base_currency, self.base_currency) {
361 pnls.insert(
362 base_currency_value,
363 Money::from_decimal(fill_qty.as_decimal(), base_currency_value)?,
364 );
365 }
366 pnls.insert(notional.currency, -notional);
367 } else {
368 if let (Some(base_currency_value), None) = (base_currency, self.base_currency) {
369 pnls.insert(
370 base_currency_value,
371 -Money::from_decimal(fill_qty.as_decimal(), base_currency_value)?,
372 );
373 }
374 pnls.insert(notional.currency, notional);
375 }
376 Ok(pnls.into_values().collect())
377 }
378
379 pub fn base_calculate_commission(
386 &self,
387 instrument: &InstrumentAny,
388 last_qty: Quantity,
389 last_px: Price,
390 liquidity_side: LiquiditySide,
391 use_quote_for_inverse: Option<bool>,
392 ) -> anyhow::Result<Money> {
393 anyhow::ensure!(
394 liquidity_side != LiquiditySide::NoLiquiditySide,
395 "Invalid `LiquiditySide`: {liquidity_side}"
396 );
397 let notional =
398 instrument.try_calculate_notional_value(last_qty, last_px, use_quote_for_inverse)?;
399 let rate = match liquidity_side {
400 LiquiditySide::Maker => instrument.maker_fee(),
401 LiquiditySide::Taker => instrument.taker_fee(),
402 LiquiditySide::NoLiquiditySide => {
403 anyhow::bail!("Invalid `LiquiditySide`: {liquidity_side}")
404 }
405 };
406 let commission = notional
407 .as_decimal()
408 .checked_mul(rate)
409 .ok_or_else(|| anyhow::anyhow!("commission calculation overflow"))?;
410
411 Ok(Money::from_decimal(commission, notional.currency)?)
412 }
413}
414
415pub(crate) fn update_balance_locked(
427 balances: &mut IndexMap<Currency, AccountBalance>,
428 balances_locked: &mut AHashMap<(InstrumentId, Currency), Money>,
429 instrument_id: InstrumentId,
430 locked: Money,
431) -> anyhow::Result<()> {
432 anyhow::ensure!(
433 !locked.is_negative(),
434 "locked balance was negative: {locked}"
435 );
436
437 let currency = locked.currency;
438 let key = (instrument_id, currency);
439
440 let Some(current_balance) = balances.get(¤cy).copied() else {
441 balances_locked.insert(key, locked);
442 return Ok(());
443 };
444
445 anyhow::ensure!(
446 current_balance.currency.precision == currency.precision,
447 "Cannot update {currency} reservation: precision {} differed from balance precision {}",
448 currency.precision,
449 current_balance.currency.precision
450 );
451
452 let previous = balances_locked.insert(key, locked);
453
454 match balance_from_locks(current_balance, balances_locked) {
455 Ok(balance) => {
456 balances.insert(currency, balance);
457 Ok(())
458 }
459 Err(e) => {
460 match previous {
462 Some(previous) => balances_locked.insert(key, previous),
463 None => balances_locked.remove(&key),
464 };
465 Err(e.into())
466 }
467 }
468}
469
470pub(crate) fn clear_balance_locked(
472 balances: &mut IndexMap<Currency, AccountBalance>,
473 balances_locked: &mut AHashMap<(InstrumentId, Currency), Money>,
474 instrument_id: InstrumentId,
475) {
476 let currencies_to_recalc: Vec<Currency> = balances_locked
477 .keys()
478 .filter(|(id, _)| *id == instrument_id)
479 .map(|(_, currency)| *currency)
480 .collect();
481
482 for currency in ¤cies_to_recalc {
483 balances_locked.remove(&(instrument_id, *currency));
484 }
485
486 for currency in currencies_to_recalc {
487 recalculate_balance(balances, balances_locked, currency);
488 }
489}
490
491pub(crate) fn recalculate_balance(
496 balances: &mut IndexMap<Currency, AccountBalance>,
497 balances_locked: &AHashMap<(InstrumentId, Currency), Money>,
498 currency: Currency,
499) {
500 let current_balance = if let Some(balance) = balances.get(¤cy) {
501 *balance
502 } else {
503 log::debug!("Cannot recalculate balance when no current balance for {currency}");
504 return;
505 };
506
507 let new_balance = match balance_from_locks(current_balance, balances_locked) {
508 Ok(balance) => balance,
509 Err(e) => {
510 log::error!(
511 "Cannot recalculate {currency} balance from reservations: {e}; using a non-spendable balance"
512 );
513 non_spendable_balance(current_balance)
514 }
515 };
516
517 balances.insert(currency, new_balance);
518}
519
520pub(crate) fn balance_from_locks(
527 current_balance: AccountBalance,
528 balances_locked: &AHashMap<(InstrumentId, Currency), Money>,
529) -> CorrectnessResult<AccountBalance> {
530 let currency = current_balance.currency;
531 let mut locked_total = Money::zero(currency);
532
533 for locked in balances_locked
534 .values()
535 .filter(|locked| locked.currency == currency)
536 {
537 check_predicate_false(
538 locked.is_negative(),
539 &format!("locked balance was negative: {locked}"),
540 )?;
541 check_predicate_true(
542 locked.currency.precision == currency.precision,
543 &format!(
544 "locked balance precision {} differed from balance precision {} for {currency}",
545 locked.currency.precision, currency.precision
546 ),
547 )?;
548
549 let reservation = if current_balance.total.is_negative() {
550 *locked
551 } else {
552 (*locked).min(current_balance.total - locked_total)
553 };
554
555 locked_total = locked_total.checked_add(reservation).ok_or_else(|| {
556 CorrectnessError::PredicateViolation {
557 message: format!("derived locked balance exceeded Money bounds for {currency}"),
558 }
559 })?;
560 }
561
562 let free = current_balance
563 .total
564 .checked_sub(locked_total)
565 .ok_or_else(|| CorrectnessError::PredicateViolation {
566 message: format!(
567 "derived free balance exceeded Money bounds for total {} and locked {locked_total}",
568 current_balance.total
569 ),
570 })?;
571
572 AccountBalance::new_checked(current_balance.total, locked_total, free)
573}
574
575fn non_spendable_balance(current_balance: AccountBalance) -> AccountBalance {
576 let zero = Money::zero(current_balance.currency);
577
578 let (locked, free) = if current_balance.total.is_negative() {
579 (zero, current_balance.total)
580 } else {
581 (current_balance.total, zero)
582 };
583
584 AccountBalance {
585 currency: current_balance.currency,
586 total: current_balance.total,
587 locked,
588 free,
589 }
590}
591
592#[cfg(all(test, feature = "test-support"))]
593mod tests {
594 use rstest::rstest;
595
596 use super::*;
597 use crate::{events::account::stubs::cash_account_state, types::money::MONEY_RAW_MAX};
598
599 #[rstest]
600 fn test_base_purge_account_events_retains_latest_when_all_purged() {
601 use crate::{
602 enums::AccountType,
603 events::account::stubs::cash_account_state,
604 identifiers::stubs::{account_id, uuid4},
605 types::{Currency, stubs::stub_account_balance},
606 };
607
608 let mut account = BaseAccount::new(cash_account_state(), true);
609
610 let event1 = AccountState::new(
612 account_id(),
613 AccountType::Cash,
614 vec![stub_account_balance()],
615 vec![],
616 true,
617 uuid4(),
618 UnixNanos::from(100_000_000),
619 UnixNanos::from(100_000_000),
620 Some(Currency::USD()),
621 );
622 let event2 = AccountState::new(
623 account_id(),
624 AccountType::Cash,
625 vec![stub_account_balance()],
626 vec![],
627 true,
628 uuid4(),
629 UnixNanos::from(200_000_000),
630 UnixNanos::from(200_000_000),
631 Some(Currency::USD()),
632 );
633 let event3 = AccountState::new(
634 account_id(),
635 AccountType::Cash,
636 vec![stub_account_balance()],
637 vec![],
638 true,
639 uuid4(),
640 UnixNanos::from(300_000_000),
641 UnixNanos::from(300_000_000),
642 Some(Currency::USD()),
643 );
644
645 account.base_apply(event1);
646 account.base_apply(event2);
647 account.base_apply(event3.clone());
648
649 assert_eq!(account.events.len(), 4);
650
651 account.base_purge_account_events(UnixNanos::from(1_000_000_000), 0);
652
653 assert_eq!(account.events.len(), 1);
654 assert_eq!(account.events[0].ts_event, event3.ts_event);
655 assert_eq!(account.base_last_event().unwrap().ts_event, event3.ts_event);
656 }
657
658 #[rstest]
659 fn test_base_purge_account_events_retains_all_for_overflowing_lookback() {
660 let mut account = BaseAccount::new(cash_account_state(), true);
661 let mut event = cash_account_state();
662 event.ts_event = UnixNanos::from(1);
663 account.base_apply(event);
664
665 account.base_purge_account_events(UnixNanos::from(u64::MAX), u64::MAX);
666
667 assert_eq!(account.events.len(), 2);
668 }
669
670 #[rstest]
671 fn test_base_purge_account_events_drops_event_exactly_at_cutoff() {
672 let mut account = BaseAccount::new(cash_account_state(), true);
673 let mut at_cutoff = cash_account_state();
674 at_cutoff.ts_event = UnixNanos::from(200_000_000_000);
675 account.base_apply(at_cutoff);
676 let mut after_cutoff = cash_account_state();
677 after_cutoff.ts_event = UnixNanos::from(200_000_000_001);
678 account.base_apply(after_cutoff);
679
680 account.base_purge_account_events(UnixNanos::from(300_000_000_000), 100);
681
682 assert_eq!(account.events.len(), 1);
683 assert_eq!(account.events[0].ts_event, UnixNanos::from(200_000_000_001));
684 }
685
686 #[rstest]
687 fn test_base_purge_account_events_retains_future_event_without_overflow() {
688 let mut event = cash_account_state();
689 event.ts_event = UnixNanos::from(u64::MAX - 1);
690 let mut account = BaseAccount::new(event, true);
691
692 account.base_purge_account_events(UnixNanos::from(u64::MAX), 60);
693
694 assert_eq!(account.events.len(), 1);
695 }
696
697 #[rstest]
698 #[should_panic(
699 expected = r#"lhs_param: "event.account_id", rhs_param: "self.id", lhs: "OTHER-001", rhs: "SIM-001""#
700 )]
701 fn test_base_apply_panics_on_different_account_id() {
702 let mut account = BaseAccount::new(cash_account_state(), true);
703 let mut event = cash_account_state();
704 event.account_id = AccountId::from("OTHER-001");
705
706 account.base_apply(event);
707 }
708
709 fn usd_balances(total: &str) -> IndexMap<Currency, AccountBalance> {
710 let total = Money::from(total);
711 let mut balances = IndexMap::new();
712 balances.insert(
713 Currency::USD(),
714 AccountBalance::new(total, Money::zero(Currency::USD()), total),
715 );
716 balances
717 }
718
719 fn mismatched_usd() -> Currency {
720 Currency::new(
721 "USD",
722 Currency::USD().precision + 1,
723 840,
724 "United States dollar",
725 crate::enums::CurrencyType::Fiat,
726 )
727 }
728
729 #[rstest]
732 #[case::observed_currency(true)]
733 #[case::unobserved_currency(false)]
734 fn test_update_balance_locked_rejects_negative_without_mutation(#[case] observed: bool) {
735 let mut balances = if observed {
736 usd_balances("1000 USD")
737 } else {
738 IndexMap::new()
739 };
740 let balances_before = balances.clone();
741 let mut balances_locked = AHashMap::new();
742 let instrument_id = InstrumentId::from("AUD/USD.SIM");
743
744 let error = update_balance_locked(
745 &mut balances,
746 &mut balances_locked,
747 instrument_id,
748 Money::from("-1 USD"),
749 )
750 .unwrap_err();
751
752 assert_eq!(error.to_string(), "locked balance was negative: -1.00 USD");
753 assert!(balances_locked.is_empty());
754 assert_eq!(balances, balances_before);
755 }
756
757 #[rstest]
758 fn test_update_balance_locked_restores_prior_reservation_on_failure() {
759 let usd = Currency::USD();
760 let mut balances = usd_balances("1000 USD");
761 let stale_key = (InstrumentId::from("EUR/USD.SIM"), mismatched_usd());
762 let stale = Money::from_decimal(Decimal::from(10), mismatched_usd()).unwrap();
763 let mut balances_locked = AHashMap::from([(stale_key, stale)]);
764 let instrument_id = InstrumentId::from("AUD/USD.SIM");
765
766 let result = update_balance_locked(
769 &mut balances,
770 &mut balances_locked,
771 instrument_id,
772 Money::from("100 USD"),
773 );
774
775 assert!(result.is_err());
776 assert_eq!(balances_locked, AHashMap::from([(stale_key, stale)]));
777 assert_eq!(balances, usd_balances("1000 USD"));
778 assert_eq!(balances[&usd].free, Money::from("1000 USD"));
779 }
780
781 #[rstest]
782 fn test_balance_from_locks_clamps_reservations_to_total() {
783 let usd = Currency::USD();
784 let total = Money::from("100 USD");
785 let current = AccountBalance::new(total, Money::zero(usd), total);
786 let mut balances_locked = AHashMap::new();
787 balances_locked.insert(
788 (InstrumentId::from("AUD/USD.SIM"), usd),
789 Money::from("60 USD"),
790 );
791 balances_locked.insert(
792 (InstrumentId::from("EUR/USD.SIM"), usd),
793 Money::from("60 USD"),
794 );
795
796 let balance = balance_from_locks(current, &balances_locked).unwrap();
797
798 assert_eq!(balance.total, total);
799 assert_eq!(balance.locked, total);
800 assert_eq!(balance.free, Money::zero(usd));
801 }
802
803 #[rstest]
804 #[case::positive_total("1000 USD", "1000 USD", "0 USD")]
805 #[case::negative_total("-1000 USD", "0 USD", "-1000 USD")]
806 fn test_recalculate_balance_degrades_to_non_spendable_for_invalid_reservation(
807 #[case] total: &str,
808 #[case] expected_locked: &str,
809 #[case] expected_free: &str,
810 ) {
811 use crate::{enums::CurrencyType, types::Currency};
812
813 let usd = Currency::USD();
814 let total = Money::from(total);
815 let mut balances = IndexMap::new();
816 balances.insert(usd, AccountBalance::new(total, Money::zero(usd), total));
817 let mismatched_usd = Currency::new(
819 "USD",
820 usd.precision + 1,
821 840,
822 "United States dollar",
823 CurrencyType::Fiat,
824 );
825 let mut balances_locked = AHashMap::new();
826 balances_locked.insert(
827 (InstrumentId::from("AUD/USD.SIM"), mismatched_usd),
828 Money::from_decimal(Decimal::from(100), mismatched_usd).unwrap(),
829 );
830
831 recalculate_balance(&mut balances, &balances_locked, usd);
832
833 let balance = balances.get(&usd).expect("balance should be retained");
834 assert_eq!(balance.total, total);
835 assert_eq!(balance.locked, Money::from(expected_locked));
836 assert_eq!(balance.free, Money::from(expected_free));
837 }
838
839 #[rstest]
840 fn test_update_commissions_sub_canonical_raw_skipped() {
841 use crate::{
842 events::account::stubs::cash_account_state,
843 types::{Currency, Money},
844 };
845
846 let mut account = BaseAccount::new(cash_account_state(), true);
847 let usd = Currency::USD();
848
849 account.update_commissions(Money::from_raw(1, usd));
851
852 assert!(account.commission(&usd).is_none());
853 }
854
855 #[rstest]
856 fn test_commissions_returns_every_currency() {
857 let mut account = BaseAccount::new(cash_account_state(), true);
858 account.update_commissions(Money::from("2.50 USD"));
859 account.update_commissions(Money::from("1.25 AUD"));
860
861 let commissions = account.commissions();
862
863 assert_eq!(commissions.len(), 2);
864 assert_eq!(
865 commissions.get(&Currency::USD()),
866 Some(&Money::from("2.50 USD"))
867 );
868 assert_eq!(
869 commissions.get(&Currency::AUD()),
870 Some(&Money::from("1.25 AUD"))
871 );
872 }
873
874 #[rstest]
875 fn test_try_update_commissions_overflow_preserves_total() {
876 let mut account = BaseAccount::new(cash_account_state(), true);
877 let usd = Currency::USD();
878 let maximum = Money::from_raw(MONEY_RAW_MAX, usd);
879
880 account.try_update_commissions(maximum).unwrap();
881 let result = account.try_update_commissions(Money::from("0.01 USD"));
882
883 assert!(result.is_err());
884 assert_eq!(account.commission(&usd), Some(maximum));
885 }
886}