Skip to main content

nautilus_model/accounts/
betting.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//! A betting account with sports-betting specific balance locking and PnL rules.
17
18use std::{
19    fmt::Display,
20    ops::{Deref, DerefMut},
21};
22
23use ahash::AHashMap;
24use indexmap::IndexMap;
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27
28use crate::{
29    accounts::{Account, base::BaseAccount},
30    enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
31    events::{AccountState, OrderFilled},
32    identifiers::{AccountId, InstrumentId},
33    instruments::{Instrument, InstrumentAny},
34    position::Position,
35    types::{AccountBalance, Currency, Money, Price, Quantity, money::MoneyRaw},
36};
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[cfg_attr(
40    feature = "python",
41    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
42)]
43#[cfg_attr(
44    feature = "python",
45    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
46)]
47pub struct BettingAccount {
48    pub base: BaseAccount,
49    /// Per-(instrument, currency) locked balances (transient, not persisted).
50    #[serde(skip, default)]
51    pub balances_locked: AHashMap<(InstrumentId, Currency), Money>,
52}
53
54impl BettingAccount {
55    /// Creates a new [`BettingAccount`] instance.
56    #[must_use]
57    pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
58        Self {
59            base: BaseAccount::new(event, calculate_account_state),
60            balances_locked: AHashMap::new(),
61        }
62    }
63
64    /// Updates the locked balance for the given instrument and currency.
65    ///
66    /// # Panics
67    ///
68    /// Panics if `locked` is negative.
69    pub fn update_balance_locked(&mut self, instrument_id: InstrumentId, locked: Money) {
70        assert!(locked.raw >= 0, "locked balance was negative: {locked}");
71        let currency = locked.currency;
72        self.balances_locked
73            .insert((instrument_id, currency), locked);
74        self.recalculate_balance(currency);
75    }
76
77    /// Clears all locked balances for the given instrument ID.
78    pub fn clear_balance_locked(&mut self, instrument_id: InstrumentId) {
79        let currencies_to_recalc: Vec<Currency> = self
80            .balances_locked
81            .keys()
82            .filter(|(id, _)| *id == instrument_id)
83            .map(|(_, currency)| *currency)
84            .collect();
85
86        for currency in &currencies_to_recalc {
87            self.balances_locked.remove(&(instrument_id, *currency));
88        }
89
90        for currency in currencies_to_recalc {
91            self.recalculate_balance(currency);
92        }
93    }
94
95    /// Updates the account balances, rejecting negative totals.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if any balance has a negative total.
100    pub fn update_balances(&mut self, balances: &[AccountBalance]) -> anyhow::Result<()> {
101        for balance in balances {
102            if balance.total.raw < 0 {
103                anyhow::bail!(
104                    "Betting account balance would become negative: {} {} ({})",
105                    balance.total.as_decimal(),
106                    balance.currency.code,
107                    self.id
108                );
109            }
110        }
111        self.base.update_balances(balances);
112        Ok(())
113    }
114
115    #[must_use]
116    pub const fn is_unleveraged(&self) -> bool {
117        true
118    }
119
120    /// Returns the balance impact for a betting order.
121    ///
122    /// For `Sell` (back) the impact is the negative stake (quantity).
123    /// For `Buy` (lay) the impact is the negative liability (quantity * (price - 1)).
124    ///
125    /// # Panics
126    ///
127    /// Panics if `order_side` is `NoOrderSide`, or if the impact cannot be represented in the
128    /// quote currency.
129    #[must_use]
130    pub fn balance_impact(
131        &self,
132        instrument: &InstrumentAny,
133        quantity: Quantity,
134        price: Price,
135        order_side: OrderSide,
136    ) -> Money {
137        let currency = instrument.quote_currency();
138        let impact = match order_side {
139            OrderSide::Sell => -quantity.as_decimal(),
140            OrderSide::Buy => -(quantity.as_decimal() * (price.as_decimal() - Decimal::ONE)),
141            OrderSide::NoOrderSide => panic!("invalid `OrderSide`, was {order_side}"),
142        };
143        Money::from_decimal(impact, currency).expect("invalid betting balance impact")
144    }
145
146    /// Recalculates the account balance for the specified currency based on per-instrument locks.
147    pub fn recalculate_balance(&mut self, currency: Currency) {
148        let current_balance = if let Some(balance) = self.balances.get(&currency) {
149            *balance
150        } else {
151            log::debug!("Cannot recalculate balance when no current balance for {currency}");
152            return;
153        };
154
155        let total_locked_raw: MoneyRaw = self
156            .balances_locked
157            .values()
158            .filter(|locked| locked.currency == currency)
159            .map(|locked| locked.raw)
160            .fold(0, |acc, raw| acc.saturating_add(raw));
161
162        let total_raw = current_balance.total.raw;
163        let (locked_raw, free_raw) = if total_locked_raw > total_raw && total_raw >= 0 {
164            (total_raw, 0)
165        } else {
166            (total_locked_raw, total_raw - total_locked_raw)
167        };
168
169        let new_balance = AccountBalance::new(
170            current_balance.total,
171            Money::from_raw(locked_raw, currency),
172            Money::from_raw(free_raw, currency),
173        );
174
175        self.balances.insert(currency, new_balance);
176    }
177}
178
179impl Account for BettingAccount {
180    fn id(&self) -> AccountId {
181        self.id
182    }
183
184    fn account_type(&self) -> AccountType {
185        self.account_type
186    }
187
188    fn base_currency(&self) -> Option<Currency> {
189        self.base_currency
190    }
191
192    fn is_cash_account(&self) -> bool {
193        true
194    }
195
196    fn is_margin_account(&self) -> bool {
197        false
198    }
199
200    fn calculated_account_state(&self) -> bool {
201        self.calculate_account_state
202    }
203
204    fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
205        self.base_balance_total(currency)
206    }
207
208    fn balances_total(&self) -> IndexMap<Currency, Money> {
209        self.base_balances_total()
210    }
211
212    fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
213        self.base_balance_free(currency)
214    }
215
216    fn balances_free(&self) -> IndexMap<Currency, Money> {
217        self.base_balances_free()
218    }
219
220    fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
221        self.base_balance_locked(currency)
222    }
223
224    fn balances_locked(&self) -> IndexMap<Currency, Money> {
225        self.base_balances_locked()
226    }
227
228    fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
229        self.base_balance(currency)
230    }
231
232    fn last_event(&self) -> Option<AccountState> {
233        self.base_last_event()
234    }
235
236    fn events(&self) -> Vec<AccountState> {
237        self.events.clone()
238    }
239
240    fn event_count(&self) -> usize {
241        self.events.len()
242    }
243
244    fn currencies(&self) -> Vec<Currency> {
245        self.balances.keys().copied().collect()
246    }
247
248    fn starting_balances(&self) -> IndexMap<Currency, Money> {
249        self.balances_starting.clone()
250    }
251
252    fn balances(&self) -> IndexMap<Currency, AccountBalance> {
253        self.balances.clone()
254    }
255
256    fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
257        for balance in &event.balances {
258            if balance.total.raw < 0 {
259                anyhow::bail!(
260                    "Cannot apply betting account state: balance would be negative {} {} ({})",
261                    balance.total.as_decimal(),
262                    balance.currency.code,
263                    self.id
264                );
265            }
266        }
267
268        if event.is_reported {
269            self.balances_locked.clear();
270        }
271
272        self.base_apply(event);
273        Ok(())
274    }
275
276    fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
277        self.base.base_purge_account_events(ts_now, lookback_secs);
278    }
279
280    fn calculate_balance_locked(
281        &mut self,
282        instrument: &InstrumentAny,
283        side: OrderSide,
284        quantity: Quantity,
285        price: Price,
286        use_quote_for_inverse: Option<bool>,
287    ) -> anyhow::Result<Money> {
288        anyhow::ensure!(
289            instrument.instrument_class() == InstrumentClass::SportsBetting,
290            "BettingAccount requires a sports betting instrument"
291        );
292        anyhow::ensure!(
293            use_quote_for_inverse != Some(true),
294            "`use_quote_for_inverse` is not applicable for betting accounts"
295        );
296
297        let locked = match side {
298            OrderSide::Sell => quantity.as_decimal(),
299            OrderSide::Buy => quantity.as_decimal() * (price.as_decimal() - Decimal::ONE),
300            OrderSide::NoOrderSide => {
301                anyhow::bail!("Invalid `OrderSide` in `calculate_balance_locked`: {side}")
302            }
303        };
304
305        Ok(Money::from_decimal(locked, instrument.quote_currency())?)
306    }
307
308    fn calculate_pnls(
309        &self,
310        instrument: &InstrumentAny,
311        fill: &OrderFilled,
312        position: Option<Position>,
313    ) -> anyhow::Result<Vec<Money>> {
314        anyhow::ensure!(
315            instrument.instrument_class() == InstrumentClass::SportsBetting,
316            "BettingAccount requires a sports betting instrument"
317        );
318
319        let mut pnls: IndexMap<Currency, Money> = IndexMap::new();
320        let quote_currency = instrument.quote_currency();
321        let base_currency = instrument.base_currency();
322
323        let mut fill_qty = fill.last_qty;
324
325        if let Some(position) = position.as_ref()
326            && position.quantity.raw != 0
327            && position.entry != fill.order_side
328        {
329            fill_qty = Quantity::from_raw(
330                fill.last_qty.raw.min(position.quantity.raw),
331                fill.last_qty.precision,
332            );
333        }
334
335        let quote_pnl = Money::from_decimal(
336            fill.last_px.as_decimal() * fill_qty.as_decimal(),
337            quote_currency,
338        )?;
339
340        match fill.order_side {
341            OrderSide::Buy => {
342                if let (Some(base_currency_value), None) = (base_currency, self.base_currency) {
343                    pnls.insert(
344                        base_currency_value,
345                        Money::from_decimal(fill_qty.as_decimal(), base_currency_value)?,
346                    );
347                }
348                pnls.insert(quote_currency, -quote_pnl);
349            }
350            OrderSide::Sell => {
351                if let (Some(base_currency_value), None) = (base_currency, self.base_currency) {
352                    pnls.insert(
353                        base_currency_value,
354                        -Money::from_decimal(fill_qty.as_decimal(), base_currency_value)?,
355                    );
356                }
357                pnls.insert(quote_currency, quote_pnl);
358            }
359            OrderSide::NoOrderSide => {
360                anyhow::bail!("Invalid `OrderSide` in calculate_pnls: {}", fill.order_side)
361            }
362        }
363
364        Ok(pnls.into_values().collect())
365    }
366
367    fn calculate_commission(
368        &self,
369        instrument: &InstrumentAny,
370        last_qty: Quantity,
371        last_px: Price,
372        liquidity_side: LiquiditySide,
373        use_quote_for_inverse: Option<bool>,
374    ) -> anyhow::Result<Money> {
375        self.base_calculate_commission(
376            instrument,
377            last_qty,
378            last_px,
379            liquidity_side,
380            use_quote_for_inverse,
381        )
382    }
383}
384
385impl Deref for BettingAccount {
386    type Target = BaseAccount;
387
388    fn deref(&self) -> &Self::Target {
389        &self.base
390    }
391}
392
393impl DerefMut for BettingAccount {
394    fn deref_mut(&mut self) -> &mut Self::Target {
395        &mut self.base
396    }
397}
398
399impl PartialEq for BettingAccount {
400    fn eq(&self, other: &Self) -> bool {
401        self.id == other.id
402    }
403}
404
405impl Eq for BettingAccount {}
406
407impl Display for BettingAccount {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        write!(
410            f,
411            "BettingAccount(id={}, type={}, base={})",
412            self.id,
413            self.account_type,
414            self.base_currency.map_or_else(
415                || "None".to_string(),
416                |base_currency| format!("{}", base_currency.code)
417            ),
418        )
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use indexmap::IndexMap;
425    use rstest::rstest;
426
427    use crate::{
428        accounts::{Account, BettingAccount, stubs::*},
429        enums::{AccountType, LiquiditySide, OrderSide},
430        events::{AccountState, account::stubs::*},
431        identifiers::AccountId,
432        instruments::{Instrument, stubs::betting},
433        orders::stubs::TestOrderEventStubs,
434        position::Position,
435        types::{AccountBalance, Currency, Money, Price, Quantity},
436    };
437
438    #[rstest]
439    fn test_display(betting_account: BettingAccount) {
440        assert_eq!(
441            format!("{betting_account}"),
442            "BettingAccount(id=SIM-001, type=BETTING, base=GBP)"
443        );
444    }
445
446    #[rstest]
447    fn test_instantiate_single_asset_betting_account(
448        betting_account: BettingAccount,
449        betting_account_state: AccountState,
450    ) {
451        assert_eq!(betting_account.id, AccountId::from("SIM-001"));
452        assert_eq!(betting_account.account_type, AccountType::Betting);
453        assert_eq!(betting_account.base_currency, Some(Currency::GBP()));
454        assert_eq!(
455            betting_account.last_event(),
456            Some(betting_account_state.clone())
457        );
458        assert_eq!(betting_account.events(), vec![betting_account_state]);
459        assert_eq!(betting_account.event_count(), 1);
460        assert_eq!(
461            betting_account.balance_total(None),
462            Some(Money::from("1000 GBP"))
463        );
464        assert_eq!(
465            betting_account.balance_free(None),
466            Some(Money::from("1000 GBP"))
467        );
468        assert_eq!(
469            betting_account.balance_locked(None),
470            Some(Money::from("0 GBP"))
471        );
472
473        let mut balances_total_expected = IndexMap::new();
474        balances_total_expected.insert(Currency::GBP(), Money::from("1000 GBP"));
475        assert_eq!(betting_account.balances_total(), balances_total_expected);
476    }
477
478    #[rstest]
479    fn test_apply_given_new_state_event_updates_correctly(
480        mut betting_account: BettingAccount,
481        betting_account_state: AccountState,
482        betting_account_state_changed: AccountState,
483    ) {
484        betting_account
485            .apply(betting_account_state_changed.clone())
486            .unwrap();
487
488        assert_eq!(
489            betting_account.last_event(),
490            Some(betting_account_state_changed.clone())
491        );
492        assert_eq!(
493            betting_account.events,
494            vec![betting_account_state, betting_account_state_changed]
495        );
496        assert_eq!(betting_account.event_count(), 2);
497        assert_eq!(
498            betting_account.balance_total(None),
499            Some(Money::from("900 GBP"))
500        );
501        assert_eq!(
502            betting_account.balance_free(None),
503            Some(Money::from("850 GBP"))
504        );
505        assert_eq!(
506            betting_account.balance_locked(None),
507            Some(Money::from("50 GBP"))
508        );
509    }
510
511    #[rstest]
512    #[case(OrderSide::Sell, "1.60", "10", "10 GBP")]
513    #[case(OrderSide::Sell, "2.00", "10", "10 GBP")]
514    #[case(OrderSide::Sell, "10.00", "20", "20 GBP")]
515    #[case(OrderSide::Buy, "1.25", "10", "2.5 GBP")]
516    #[case(OrderSide::Buy, "2.00", "10", "10 GBP")]
517    #[case(OrderSide::Buy, "10.00", "10", "90 GBP")]
518    fn test_calculate_balance_locked(
519        mut betting_account: BettingAccount,
520        betting: crate::instruments::BettingInstrument,
521        #[case] side: OrderSide,
522        #[case] price: &str,
523        #[case] quantity: &str,
524        #[case] expected: &str,
525    ) {
526        let result = betting_account
527            .calculate_balance_locked(
528                &betting.into_any(),
529                side,
530                Quantity::from(quantity),
531                Price::from(price),
532                None,
533            )
534            .unwrap();
535        assert_eq!(result, Money::from(expected));
536    }
537
538    #[rstest]
539    fn test_calculate_pnls_single_currency_account(
540        betting_account: BettingAccount,
541        betting: crate::instruments::BettingInstrument,
542    ) {
543        let order = crate::orders::builder::OrderTestBuilder::new(crate::enums::OrderType::Market)
544            .instrument_id(betting.id())
545            .side(OrderSide::Buy)
546            .quantity(Quantity::from("100"))
547            .build();
548        let betting_any = betting.into_any();
549        let fill = TestOrderEventStubs::filled(
550            &order,
551            &betting_any,
552            None,
553            None,
554            Some(Price::from("0.8")),
555            None,
556            None,
557            None,
558            None,
559            Some(AccountId::from("SIM-001")),
560        );
561        let position = Position::new(&betting_any, fill.clone().into());
562        let fill_owned: crate::events::OrderFilled = fill.into();
563
564        let result = betting_account
565            .calculate_pnls(&betting_any, &fill_owned, Some(position))
566            .unwrap();
567
568        assert_eq!(result, vec![Money::from("-80 GBP")]);
569    }
570
571    #[rstest]
572    fn test_calculate_pnls_partially_closed(
573        betting_account: BettingAccount,
574        betting: crate::instruments::BettingInstrument,
575    ) {
576        let order1 = crate::orders::builder::OrderTestBuilder::new(crate::enums::OrderType::Market)
577            .instrument_id(betting.id())
578            .side(OrderSide::Buy)
579            .quantity(Quantity::from("100"))
580            .build();
581        let betting_any = betting.clone().into_any();
582        let fill1 = TestOrderEventStubs::filled(
583            &order1,
584            &betting_any,
585            None,
586            None,
587            Some(Price::from("0.5")),
588            None,
589            None,
590            None,
591            None,
592            Some(AccountId::from("SIM-001")),
593        );
594
595        let order2 = crate::orders::builder::OrderTestBuilder::new(crate::enums::OrderType::Market)
596            .instrument_id(betting.id())
597            .side(OrderSide::Sell)
598            .quantity(Quantity::from("50"))
599            .build();
600        let fill2 = TestOrderEventStubs::filled(
601            &order2,
602            &betting_any,
603            None,
604            None,
605            Some(Price::from("0.8")),
606            None,
607            None,
608            None,
609            None,
610            Some(AccountId::from("SIM-001")),
611        );
612
613        let position = Position::new(&betting_any, fill1.into());
614        let fill2_owned: crate::events::OrderFilled = fill2.into();
615        let result = betting_account
616            .calculate_pnls(&betting_any, &fill2_owned, Some(position))
617            .unwrap();
618
619        assert_eq!(result, vec![Money::from("40 GBP")]);
620    }
621
622    #[rstest]
623    fn test_calculate_commission_invalid_liquidity_side_raises(
624        betting_account: BettingAccount,
625        betting: crate::instruments::BettingInstrument,
626    ) {
627        let result = betting_account.calculate_commission(
628            &betting.into_any(),
629            Quantity::from("1"),
630            Price::from("1"),
631            LiquiditySide::NoLiquiditySide,
632            None,
633        );
634        assert!(
635            result
636                .unwrap_err()
637                .to_string()
638                .contains("Invalid `LiquiditySide`: NO_LIQUIDITY_SIDE")
639        );
640    }
641
642    #[rstest]
643    #[case(OrderSide::Buy, "5.0", "100", "-400 GBP")]
644    #[case(OrderSide::Buy, "1.5", "100", "-50 GBP")]
645    #[case(OrderSide::Sell, "5.0", "100", "-100 GBP")]
646    #[case(OrderSide::Sell, "10.0", "100", "-100 GBP")]
647    fn test_balance_impact(
648        betting_account: BettingAccount,
649        betting: crate::instruments::BettingInstrument,
650        #[case] side: OrderSide,
651        #[case] price: &str,
652        #[case] quantity: &str,
653        #[case] expected: &str,
654    ) {
655        let impact = betting_account.balance_impact(
656            &betting.into_any(),
657            Quantity::from(quantity),
658            Price::from(price),
659            side,
660        );
661
662        assert_eq!(impact, Money::from(expected));
663    }
664
665    #[rstest]
666    fn test_apply_rejects_negative_balance(mut betting_account: BettingAccount) {
667        let negative_state = AccountState::new(
668            AccountId::from("SIM-001"),
669            AccountType::Betting,
670            vec![AccountBalance::new(
671                Money::from("-50 GBP"),
672                Money::from("0 GBP"),
673                Money::from("-50 GBP"),
674            )],
675            vec![],
676            false,
677            crate::identifiers::stubs::uuid4(),
678            0.into(),
679            0.into(),
680            Some(Currency::GBP()),
681        );
682
683        let result = betting_account.apply(negative_state);
684        assert!(result.is_err());
685        assert!(
686            result
687                .unwrap_err()
688                .to_string()
689                .contains("balance would be negative")
690        );
691    }
692
693    #[rstest]
694    fn test_update_balances_rejects_negative_total(mut betting_account: BettingAccount) {
695        let result = betting_account.update_balances(&[AccountBalance::new(
696            Money::from("-10 GBP"),
697            Money::from("0 GBP"),
698            Money::from("-10 GBP"),
699        )]);
700
701        assert!(result.is_err());
702    }
703
704    #[rstest]
705    fn test_recalculate_balance_clamps_locked_to_total(mut betting_account: BettingAccount) {
706        let instrument_id =
707            crate::identifiers::InstrumentId::from("BETFAIR-1.2345678-12345678-0.0.NONE");
708
709        betting_account.update_balance_locked(instrument_id, Money::from("1500 GBP"));
710
711        let balance = betting_account.balance(Some(Currency::GBP())).unwrap();
712        assert_eq!(balance.locked, Money::from("1000 GBP"));
713        assert_eq!(balance.free, Money::from("0 GBP"));
714        assert_eq!(balance.total, Money::from("1000 GBP"));
715    }
716
717    #[rstest]
718    fn test_calculate_pnls_sell_fill(
719        betting_account: BettingAccount,
720        betting: crate::instruments::BettingInstrument,
721    ) {
722        let order = crate::orders::builder::OrderTestBuilder::new(crate::enums::OrderType::Market)
723            .instrument_id(betting.id())
724            .side(OrderSide::Sell)
725            .quantity(Quantity::from("100"))
726            .build();
727        let betting_any = betting.into_any();
728        let fill = TestOrderEventStubs::filled(
729            &order,
730            &betting_any,
731            None,
732            None,
733            Some(Price::from("0.8")),
734            None,
735            None,
736            None,
737            None,
738            Some(AccountId::from("SIM-001")),
739        );
740        let position = Position::new(&betting_any, fill.clone().into());
741        let fill_owned: crate::events::OrderFilled = fill.into();
742
743        let result = betting_account
744            .calculate_pnls(&betting_any, &fill_owned, Some(position))
745            .unwrap();
746
747        assert_eq!(result, vec![Money::from("80 GBP")]);
748    }
749
750    #[rstest]
751    fn test_calculate_balance_locked_rejects_non_betting_instrument(
752        mut betting_account: BettingAccount,
753    ) {
754        let audusd = crate::instruments::stubs::audusd_sim();
755        let result = betting_account.calculate_balance_locked(
756            &audusd.into(),
757            OrderSide::Buy,
758            Quantity::from("100"),
759            Price::from("1.5"),
760            None,
761        );
762
763        assert!(result.is_err());
764        assert!(result.unwrap_err().to_string().contains("sports betting"));
765    }
766}