Skip to main content

nautilus_model/python/account/
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
16use indexmap::IndexMap;
17use nautilus_core::{
18    UnixNanos,
19    python::{
20        IntoPyObjectNautilusExt, correctness_error_to_pyvalue_err, to_pyruntime_err, to_pyvalue_err,
21    },
22};
23use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
24use rust_decimal::Decimal;
25
26use crate::{
27    accounts::{Account, BettingAccount},
28    enums::{AccountType, LiquiditySide, OrderSide},
29    events::{AccountState, OrderFilled},
30    fees::MakerTakerFeeRates,
31    identifiers::AccountId,
32    position::Position,
33    python::{account::resolve_balance_currency, instruments::pyobject_to_instrument_any},
34    types::{AccountBalance, Currency, Money, Price, Quantity},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl BettingAccount {
40    /// Represents a betting account that stakes on sports betting markets.
41    #[new]
42    #[pyo3(signature = (event, calculate_account_state))]
43    #[must_use]
44    pub fn py_new(event: AccountState, calculate_account_state: bool) -> Self {
45        Self::new(event, calculate_account_state)
46    }
47
48    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
49        match op {
50            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
51            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
52            _ => py.NotImplemented(),
53        }
54    }
55
56    fn __repr__(&self) -> String {
57        format!(
58            "{}(id={}, type={}, base={})",
59            stringify!(BettingAccount),
60            self.id,
61            self.account_type,
62            self.base_currency.map_or_else(
63                || "None".to_string(),
64                |base_currency| format!("{}", base_currency.code)
65            ),
66        )
67    }
68
69    #[getter]
70    #[pyo3(name = "id")]
71    fn py_id(&self) -> AccountId {
72        self.id
73    }
74
75    #[getter]
76    #[pyo3(name = "account_type")]
77    fn py_account_type(&self) -> AccountType {
78        self.account_type
79    }
80
81    #[getter]
82    #[pyo3(name = "base_currency")]
83    fn py_base_currency(&self) -> Option<Currency> {
84        self.base_currency
85    }
86
87    #[getter]
88    #[pyo3(name = "last_event")]
89    fn py_last_event(&self) -> Option<AccountState> {
90        self.last_event()
91    }
92
93    #[getter]
94    #[pyo3(name = "event_count")]
95    fn py_event_count(&self) -> usize {
96        self.event_count()
97    }
98
99    #[getter]
100    #[pyo3(name = "events")]
101    fn py_events(&self) -> Vec<AccountState> {
102        self.events()
103    }
104
105    #[getter]
106    #[pyo3(name = "calculate_account_state")]
107    fn py_calculate_account_state(&self) -> bool {
108        self.calculate_account_state
109    }
110
111    #[pyo3(name = "balance_total")]
112    #[pyo3(signature = (currency=None))]
113    fn py_balance_total(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
114        let currency = resolve_balance_currency(currency, self.base_currency)?;
115        Ok(self.balance_total(Some(currency)))
116    }
117
118    #[pyo3(name = "balances_total")]
119    fn py_balances_total(&self) -> IndexMap<Currency, Money> {
120        self.balances_total()
121    }
122
123    #[pyo3(name = "balance_free")]
124    #[pyo3(signature = (currency=None))]
125    fn py_balance_free(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
126        let currency = resolve_balance_currency(currency, self.base_currency)?;
127        Ok(self.balance_free(Some(currency)))
128    }
129
130    #[pyo3(name = "balances_free")]
131    fn py_balances_free(&self) -> IndexMap<Currency, Money> {
132        self.balances_free()
133    }
134
135    #[pyo3(name = "balance_locked")]
136    #[pyo3(signature = (currency=None))]
137    fn py_balance_locked(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
138        let currency = resolve_balance_currency(currency, self.base_currency)?;
139        Ok(self.balance_locked(Some(currency)))
140    }
141
142    #[pyo3(name = "balances_locked")]
143    fn py_balances_locked(&self) -> IndexMap<Currency, Money> {
144        self.balances_locked()
145    }
146
147    #[pyo3(name = "balance")]
148    #[pyo3(signature = (currency=None))]
149    fn py_balance(&self, currency: Option<Currency>) -> PyResult<Option<AccountBalance>> {
150        let currency = resolve_balance_currency(currency, self.base_currency)?;
151        Ok(Account::balance(self, Some(currency)).copied())
152    }
153
154    #[pyo3(name = "balances")]
155    fn py_balances(&self) -> IndexMap<Currency, AccountBalance> {
156        Account::balances(self)
157    }
158
159    #[pyo3(name = "starting_balances")]
160    fn py_starting_balances(&self) -> IndexMap<Currency, Money> {
161        Account::starting_balances(self)
162    }
163
164    #[pyo3(name = "currencies")]
165    fn py_currencies(&self) -> Vec<Currency> {
166        Account::currencies(self)
167    }
168
169    #[pyo3(name = "is_cash_account")]
170    fn py_is_cash_account(&self) -> bool {
171        Account::is_cash_account(self)
172    }
173
174    #[pyo3(name = "is_margin_account")]
175    fn py_is_margin_account(&self) -> bool {
176        Account::is_margin_account(self)
177    }
178
179    #[pyo3(name = "purge_account_events")]
180    fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
181        Account::purge_account_events(self, UnixNanos::from(ts_now), lookback_secs);
182    }
183
184    #[pyo3(name = "apply")]
185    fn py_apply(&mut self, event: AccountState) -> PyResult<()> {
186        self.apply(event).map_err(to_pyruntime_err)
187    }
188
189    #[pyo3(name = "calculate_balance_locked")]
190    #[pyo3(signature = (instrument, side, quantity, price, use_quote_for_inverse=None))]
191    fn py_calculate_balance_locked(
192        &self,
193        instrument: Py<PyAny>,
194        side: OrderSide,
195        quantity: Quantity,
196        price: Price,
197        use_quote_for_inverse: Option<bool>,
198        py: Python,
199    ) -> PyResult<Money> {
200        let instrument = pyobject_to_instrument_any(py, instrument)?;
201        self.calculate_balance_locked(&instrument, side, quantity, price, use_quote_for_inverse)
202            .map_err(to_pyvalue_err)
203    }
204
205    #[pyo3(name = "calculate_commission")]
206    #[pyo3(signature = (instrument, last_qty, last_px, liquidity_side, maker_rate, taker_rate, use_quote_for_inverse=None))]
207    #[expect(clippy::too_many_arguments)]
208    fn py_calculate_commission(
209        &self,
210        instrument: Py<PyAny>,
211        last_qty: Quantity,
212        last_px: Price,
213        liquidity_side: LiquiditySide,
214        maker_rate: Decimal,
215        taker_rate: Decimal,
216        use_quote_for_inverse: Option<bool>,
217        py: Python,
218    ) -> PyResult<Money> {
219        if liquidity_side == LiquiditySide::NoLiquiditySide {
220            return Err(to_pyvalue_err("Invalid liquidity side"));
221        }
222        let instrument = pyobject_to_instrument_any(py, instrument)?;
223        let fee_rates = MakerTakerFeeRates::new(maker_rate, taker_rate);
224        self.calculate_commission(
225            &instrument,
226            last_qty,
227            last_px,
228            liquidity_side,
229            fee_rates,
230            use_quote_for_inverse,
231        )
232        .map_err(to_pyvalue_err)
233    }
234
235    #[pyo3(name = "calculate_pnls")]
236    #[pyo3(signature = (instrument, fill, position=None))]
237    fn py_calculate_pnls(
238        &self,
239        instrument: Py<PyAny>,
240        fill: &OrderFilled,
241        position: Option<Position>,
242        py: Python,
243    ) -> PyResult<Vec<Money>> {
244        let instrument = pyobject_to_instrument_any(py, instrument)?;
245        self.calculate_pnls(&instrument, fill, position)
246            .map_err(to_pyvalue_err)
247    }
248
249    /// Returns the balance impact for a betting order.
250    ///
251    /// For `Sell` (back) the impact is the negative stake (quantity).
252    /// For `Buy` (lay) the impact is the negative liability (quantity * (price - 1)).
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the impact cannot be represented in the quote currency.
257    #[pyo3(name = "balance_impact")]
258    fn py_balance_impact(
259        &self,
260        instrument: Py<PyAny>,
261        quantity: Quantity,
262        price: Price,
263        order_side: OrderSide,
264        py: Python,
265    ) -> PyResult<Money> {
266        let instrument = pyobject_to_instrument_any(py, instrument)?;
267        self.balance_impact(&instrument, quantity, price, order_side)
268            .map_err(correctness_error_to_pyvalue_err)
269    }
270
271    #[pyo3(name = "to_dict")]
272    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
273        let dict = PyDict::new(py);
274        dict.set_item("type", "BettingAccount")?;
275        dict.set_item("calculate_account_state", self.calculate_account_state)?;
276        let events_list: PyResult<Vec<Py<PyAny>>> =
277            self.events.iter().map(|item| item.py_to_dict(py)).collect();
278        dict.set_item("events", events_list?)?;
279        Ok(dict.into())
280    }
281}