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