nautilus_model/python/data/
bet.rs

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