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