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