Skip to main content

nautilus_model/python/data/
bet.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 std::{
17    collections::hash_map::DefaultHasher,
18    hash::{Hash, Hasher},
19};
20
21use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
22use pyo3::{basic::CompareOp, prelude::*};
23use rust_decimal::Decimal;
24
25use crate::{
26    data::bet::{
27        Bet, BetPosition, calc_bets_pnl_checked, inverse_probability_to_bet, probability_to_bet,
28    },
29    enums::{BetSide, OrderSide},
30};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl Bet {
35    /// A bet in a betting market.
36    #[new]
37    fn py_new(price: Decimal, stake: Decimal, side: BetSide) -> Self {
38        Self::new(price, stake, side)
39    }
40
41    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
42        match op {
43            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
44            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
45            _ => py.NotImplemented(),
46        }
47    }
48
49    fn __hash__(&self) -> isize {
50        let mut h = DefaultHasher::new();
51        self.hash(&mut h);
52        h.finish() as isize
53    }
54
55    fn __repr__(&self) -> String {
56        format!("{self:?}")
57    }
58
59    fn __str__(&self) -> String {
60        self.to_string()
61    }
62
63    /// Creates a bet from a stake or liability depending on the bet side.
64    ///
65    /// For `BetSide::Back` this calls `Self.from_stake` and for
66    /// `BetSide::Lay` it calls `Self.from_liability`.
67    #[staticmethod]
68    #[pyo3(name = "from_stake_or_liability")]
69    fn py_from_stake_or_liability(
70        price: Decimal,
71        volume: Decimal,
72        side: BetSide,
73    ) -> PyResult<Self> {
74        Self::from_stake_or_liability_checked(price, volume, side).map_err(to_pyvalue_err)
75    }
76
77    /// Creates a bet from a given stake.
78    #[staticmethod]
79    #[pyo3(name = "from_stake")]
80    fn py_from_stake(price: Decimal, stake: Decimal, side: BetSide) -> Self {
81        Self::from_stake(price, stake, side)
82    }
83
84    /// Creates a bet from a given liability.
85    #[staticmethod]
86    #[pyo3(name = "from_liability")]
87    fn py_from_liability(price: Decimal, liability: Decimal, side: BetSide) -> PyResult<Self> {
88        Self::from_liability_checked(price, liability, side).map_err(to_pyvalue_err)
89    }
90
91    /// Returns the bet's price.
92    #[getter]
93    #[pyo3(name = "price")]
94    fn py_price(&self) -> Decimal {
95        self.price()
96    }
97
98    /// Returns the bet's stake.
99    #[getter]
100    #[pyo3(name = "stake")]
101    fn py_stake(&self) -> Decimal {
102        self.stake()
103    }
104
105    /// Returns the bet's side.
106    #[getter]
107    #[pyo3(name = "side")]
108    fn py_side(&self) -> BetSide {
109        self.side()
110    }
111
112    /// Returns the bet's exposure.
113    ///
114    /// For BACK bets, exposure is positive; for LAY bets, it is negative.
115    #[pyo3(name = "exposure")]
116    fn py_exposure(&self) -> PyResult<Decimal> {
117        self.exposure_checked().map_err(to_pyvalue_err)
118    }
119
120    /// Returns the bet's liability.
121    ///
122    /// For BACK bets, liability equals the stake; for LAY bets, it is
123    /// stake multiplied by (price - 1).
124    #[pyo3(name = "liability")]
125    fn py_liability(&self) -> PyResult<Decimal> {
126        self.liability_checked().map_err(to_pyvalue_err)
127    }
128
129    /// Returns the bet's profit.
130    ///
131    /// For BACK bets, profit is stake * (price - 1); for LAY bets it equals the stake.
132    #[pyo3(name = "profit")]
133    fn py_profit(&self) -> PyResult<Decimal> {
134        self.profit_checked().map_err(to_pyvalue_err)
135    }
136
137    /// Returns the outcome win payoff.
138    ///
139    /// For BACK bets this is the profit; for LAY bets it is the negative liability.
140    #[pyo3(name = "outcome_win_payoff")]
141    fn py_outcome_win_payoff(&self) -> PyResult<Decimal> {
142        self.outcome_win_payoff_checked().map_err(to_pyvalue_err)
143    }
144
145    /// Returns the outcome lose payoff.
146    ///
147    /// For BACK bets this is the negative liability; for LAY bets it is the profit.
148    #[pyo3(name = "outcome_lose_payoff")]
149    fn py_outcome_lose_payoff(&self) -> PyResult<Decimal> {
150        self.outcome_lose_payoff_checked().map_err(to_pyvalue_err)
151    }
152
153    /// Returns the hedging stake given a new price.
154    #[pyo3(name = "hedging_stake")]
155    fn py_hedging_stake(&self, price: Decimal) -> PyResult<Decimal> {
156        self.hedging_stake_checked(price).map_err(to_pyvalue_err)
157    }
158
159    /// Creates a hedging bet for a given price.
160    #[pyo3(name = "hedging_bet")]
161    fn py_hedging_bet(&self, price: Decimal) -> PyResult<Self> {
162        self.hedging_bet_checked(price).map_err(to_pyvalue_err)
163    }
164}
165
166#[pymethods]
167#[pyo3_stub_gen::derive::gen_stub_pymethods]
168impl BetPosition {
169    /// A position comprising one or more bets.
170    #[new]
171    fn py_new() -> Self {
172        Self::default()
173    }
174
175    fn __repr__(&self) -> String {
176        format!("{self:?}")
177    }
178
179    fn __str__(&self) -> String {
180        self.to_string()
181    }
182
183    /// Returns the position's price.
184    #[getter]
185    #[pyo3(name = "price")]
186    fn py_price(&self) -> Decimal {
187        self.price()
188    }
189
190    /// Returns the overall side of the position.
191    ///
192    /// If exposure is positive the side is BACK; if negative, LAY; if zero, None.
193    #[getter]
194    #[pyo3(name = "side")]
195    fn py_side(&self) -> Option<BetSide> {
196        self.side()
197    }
198
199    /// Returns the position's exposure.
200    #[getter]
201    #[pyo3(name = "exposure")]
202    fn py_exposure(&self) -> Decimal {
203        self.exposure()
204    }
205
206    /// Returns the position's realized profit and loss.
207    #[getter]
208    #[pyo3(name = "realized_pnl")]
209    fn py_realized_pnl(&self) -> Decimal {
210        self.realized_pnl()
211    }
212
213    /// Adds a bet to the position, adjusting exposure and realized PnL.
214    #[pyo3(name = "add_bet")]
215    fn py_add_bet(&mut self, bet: &Bet) -> PyResult<()> {
216        self.add_bet_checked(bet.clone()).map_err(to_pyvalue_err)
217    }
218
219    /// Converts the current position into a single bet, if possible.
220    #[pyo3(name = "as_bet")]
221    fn py_as_bet(&self) -> PyResult<Option<Bet>> {
222        self.as_bet_checked().map_err(to_pyvalue_err)
223    }
224
225    /// Calculates the unrealized profit and loss given a current price.
226    #[pyo3(name = "unrealized_pnl")]
227    fn py_unrealized_pnl(&self, price: Decimal) -> PyResult<Decimal> {
228        self.unrealized_pnl_checked(price).map_err(to_pyvalue_err)
229    }
230
231    /// Returns the total profit and loss (realized plus unrealized) given a current price.
232    #[pyo3(name = "total_pnl")]
233    fn py_total_pnl(&self, price: Decimal) -> PyResult<Decimal> {
234        self.total_pnl_checked(price).map_err(to_pyvalue_err)
235    }
236
237    /// Creates a bet that would flatten (neutralize) the current position.
238    #[pyo3(name = "flattening_bet")]
239    fn py_flattening_bet(&self, price: Decimal) -> PyResult<Option<Bet>> {
240        self.flattening_bet_checked(price).map_err(to_pyvalue_err)
241    }
242
243    /// Resets the bet position to its initial state.
244    #[pyo3(name = "reset")]
245    fn py_reset(&mut self) {
246        self.reset();
247    }
248}
249
250/// Calculates the combined profit and loss for a slice of bets.
251#[pyfunction]
252#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
253#[pyo3(name = "calc_bets_pnl")]
254#[expect(clippy::needless_pass_by_value)]
255pub fn py_calc_bets_pnl(bets: Vec<Bet>) -> PyResult<Decimal> {
256    calc_bets_pnl_checked(&bets).map_err(to_pyvalue_err)
257}
258
259/// Converts a probability and volume into a Bet.
260///
261/// For a BUY side, this creates a BACK bet; for SELL, a LAY bet.
262///
263/// # Errors
264///
265/// Returns an error if `probability` is zero or the conversion overflows.
266#[pyfunction]
267#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
268#[pyo3(name = "probability_to_bet")]
269pub fn py_probability_to_bet(
270    probability: Decimal,
271    volume: Decimal,
272    side: OrderSide,
273) -> PyResult<Bet> {
274    probability_to_bet(probability, volume, side).map_err(to_pyvalue_err)
275}
276
277/// Converts a probability and volume into a Bet using the inverse probability.
278///
279/// The side is also inverted (BUY becomes SELL and vice versa).
280///
281/// # Errors
282///
283/// Returns an error if `probability` is 1.0 or its inverse is zero.
284#[pyfunction]
285#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
286#[pyo3(name = "inverse_probability_to_bet")]
287pub fn py_inverse_probability_to_bet(
288    probability: Decimal,
289    volume: Decimal,
290    side: OrderSide,
291) -> PyResult<Bet> {
292    inverse_probability_to_bet(probability, volume, side).map_err(to_pyvalue_err)
293}