nautilus_model/instruments/
betting.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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::hash::{Hash, Hasher};
17
18use nautilus_core::{
19    UnixNanos,
20    correctness::{FAILED, check_equal_u8},
21};
22use rust_decimal::Decimal;
23use rust_decimal_macros::dec;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Instrument, any::InstrumentAny};
28use crate::{
29    enums::{AssetClass, InstrumentClass, OptionKind},
30    identifiers::{InstrumentId, Symbol},
31    types::{
32        currency::Currency,
33        money::Money,
34        price::{Price, check_positive_price},
35        quantity::{Quantity, check_positive_quantity},
36    },
37};
38
39/// Represents a betting instrument with complete market and selection details.
40#[repr(C)]
41#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.model")
45)]
46pub struct BettingInstrument {
47    /// The instrument ID.
48    pub id: InstrumentId,
49    /// The raw/local/native symbol for the instrument, assigned by the venue.
50    pub raw_symbol: Symbol,
51    /// The event type identifier (e.g. 1=Soccer, 2=Tennis).
52    pub event_type_id: u64,
53    /// The name of the event type (e.g. "Soccer", "Tennis").
54    pub event_type_name: Ustr,
55    /// The competition/league identifier.
56    pub competition_id: u64,
57    /// The name of the competition (e.g. "English Premier League").
58    pub competition_name: Ustr,
59    /// The unique identifier for the event.
60    pub event_id: u64,
61    /// The name of the event (e.g. "Arsenal vs Chelsea").
62    pub event_name: Ustr,
63    /// The ISO country code where the event takes place.
64    pub event_country_code: Ustr,
65    /// UNIX timestamp (nanoseconds) when the event becomes available for betting.
66    pub event_open_date: UnixNanos,
67    /// The type of betting (e.g. "ODDS", "LINE").
68    pub betting_type: Ustr,
69    /// The unique identifier for the betting market.
70    pub market_id: Ustr,
71    /// The name of the market (e.g. "Match Odds", "Total Goals").
72    pub market_name: Ustr,
73    /// The type of market (e.g. "WIN", "PLACE").
74    pub market_type: Ustr,
75    /// UNIX timestamp (nanoseconds) when betting starts for this market.
76    pub market_start_time: UnixNanos,
77    /// The unique identifier for the selection within the market.
78    pub selection_id: u64,
79    /// The name of the selection (e.g. "Arsenal", "Over 2.5").
80    pub selection_name: Ustr,
81    /// The handicap value for the selection, if applicable.
82    pub selection_handicap: f64,
83    /// The contract currency.
84    pub currency: Currency,
85    /// The price decimal precision.
86    pub price_precision: u8,
87    /// The trading size decimal precision.
88    pub size_precision: u8,
89    /// The minimum price increment (tick size).
90    pub price_increment: Price,
91    /// The minimum size increment.
92    pub size_increment: Quantity,
93    /// The initial (order) margin requirement in percentage of order value.
94    pub margin_init: Decimal,
95    /// The maintenance (position) margin in percentage of position value.
96    pub margin_maint: Decimal,
97    /// The fee rate for liquidity makers as a percentage of order value.
98    pub maker_fee: Decimal,
99    /// The fee rate for liquidity takers as a percentage of order value.
100    pub taker_fee: Decimal,
101    /// The maximum allowable order quantity.
102    pub max_quantity: Option<Quantity>,
103    /// The minimum allowable order quantity.
104    pub min_quantity: Option<Quantity>,
105    /// The maximum allowable order notional value.
106    pub max_notional: Option<Money>,
107    /// The minimum allowable order notional value.
108    pub min_notional: Option<Money>,
109    /// The maximum allowable quoted price.
110    pub max_price: Option<Price>,
111    /// The minimum allowable quoted price.
112    pub min_price: Option<Price>,
113    /// UNIX timestamp (nanoseconds) when the data event occurred.
114    pub ts_event: UnixNanos,
115    /// UNIX timestamp (nanoseconds) when the data object was initialized.
116    pub ts_init: UnixNanos,
117}
118
119impl BettingInstrument {
120    /// Creates a new [`BettingInstrument`] instance with correctness checking.
121    ///
122    /// # Notes
123    ///
124    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
125    /// # Errors
126    ///
127    /// Returns an error if any input validation fails (precision mismatches or non-positive increments).
128    #[allow(clippy::too_many_arguments)]
129    pub fn new_checked(
130        instrument_id: InstrumentId,
131        raw_symbol: Symbol,
132        event_type_id: u64,
133        event_type_name: Ustr,
134        competition_id: u64,
135        competition_name: Ustr,
136        event_id: u64,
137        event_name: Ustr,
138        event_country_code: Ustr,
139        event_open_date: UnixNanos,
140        betting_type: Ustr,
141        market_id: Ustr,
142        market_name: Ustr,
143        market_type: Ustr,
144        market_start_time: UnixNanos,
145        selection_id: u64,
146        selection_name: Ustr,
147        selection_handicap: f64,
148        currency: Currency,
149        price_precision: u8,
150        size_precision: u8,
151        price_increment: Price,
152        size_increment: Quantity,
153        max_quantity: Option<Quantity>,
154        min_quantity: Option<Quantity>,
155        max_notional: Option<Money>,
156        min_notional: Option<Money>,
157        max_price: Option<Price>,
158        min_price: Option<Price>,
159        margin_init: Option<Decimal>,
160        margin_maint: Option<Decimal>,
161        maker_fee: Option<Decimal>,
162        taker_fee: Option<Decimal>,
163        ts_event: UnixNanos,
164        ts_init: UnixNanos,
165    ) -> anyhow::Result<Self> {
166        check_equal_u8(
167            price_precision,
168            price_increment.precision,
169            stringify!(price_precision),
170            stringify!(price_increment.precision),
171        )?;
172        check_equal_u8(
173            size_precision,
174            size_increment.precision,
175            stringify!(size_precision),
176            stringify!(size_increment.precision),
177        )?;
178        check_positive_price(price_increment, stringify!(price_increment))?;
179        check_positive_quantity(size_increment, stringify!(size_increment))?;
180
181        Ok(Self {
182            id: instrument_id,
183            raw_symbol,
184            event_type_id,
185            event_type_name,
186            competition_id,
187            competition_name,
188            event_id,
189            event_name,
190            event_country_code,
191            event_open_date,
192            betting_type,
193            market_id,
194            market_name,
195            market_type,
196            market_start_time,
197            selection_id,
198            selection_name,
199            selection_handicap,
200            currency,
201            price_precision,
202            size_precision,
203            price_increment,
204            size_increment,
205            max_quantity,
206            min_quantity,
207            max_notional,
208            min_notional,
209            max_price,
210            min_price,
211            margin_init: margin_init.unwrap_or(dec!(1)),
212            margin_maint: margin_maint.unwrap_or(dec!(1)),
213            maker_fee: maker_fee.unwrap_or_default(),
214            taker_fee: taker_fee.unwrap_or_default(),
215            ts_event,
216            ts_init,
217        })
218    }
219
220    /// Creates a new [`BettingInstrument`] instance by parsing and validating input parameters.
221    ///
222    /// # Panics
223    ///
224    /// Panics if any required parameter is invalid or parsing fails during `new_checked`.
225    #[allow(clippy::too_many_arguments)]
226    pub fn new(
227        instrument_id: InstrumentId,
228        raw_symbol: Symbol,
229        event_type_id: u64,
230        event_type_name: Ustr,
231        competition_id: u64,
232        competition_name: Ustr,
233        event_id: u64,
234        event_name: Ustr,
235        event_country_code: Ustr,
236        event_open_date: UnixNanos,
237        betting_type: Ustr,
238        market_id: Ustr,
239        market_name: Ustr,
240        market_type: Ustr,
241        market_start_time: UnixNanos,
242        selection_id: u64,
243        selection_name: Ustr,
244        selection_handicap: f64,
245        currency: Currency,
246        price_precision: u8,
247        size_precision: u8,
248        price_increment: Price,
249        size_increment: Quantity,
250        max_quantity: Option<Quantity>,
251        min_quantity: Option<Quantity>,
252        max_notional: Option<Money>,
253        min_notional: Option<Money>,
254        max_price: Option<Price>,
255        min_price: Option<Price>,
256        margin_init: Option<Decimal>,
257        margin_maint: Option<Decimal>,
258        maker_fee: Option<Decimal>,
259        taker_fee: Option<Decimal>,
260        ts_event: UnixNanos,
261        ts_init: UnixNanos,
262    ) -> Self {
263        Self::new_checked(
264            instrument_id,
265            raw_symbol,
266            event_type_id,
267            event_type_name,
268            competition_id,
269            competition_name,
270            event_id,
271            event_name,
272            event_country_code,
273            event_open_date,
274            betting_type,
275            market_id,
276            market_name,
277            market_type,
278            market_start_time,
279            selection_id,
280            selection_name,
281            selection_handicap,
282            currency,
283            price_precision,
284            size_precision,
285            price_increment,
286            size_increment,
287            max_quantity,
288            min_quantity,
289            max_notional,
290            min_notional,
291            max_price,
292            min_price,
293            margin_init,
294            margin_maint,
295            maker_fee,
296            taker_fee,
297            ts_event,
298            ts_init,
299        )
300        .expect(FAILED)
301    }
302}
303
304impl PartialEq<Self> for BettingInstrument {
305    fn eq(&self, other: &Self) -> bool {
306        self.id == other.id
307    }
308}
309
310impl Eq for BettingInstrument {}
311
312impl Hash for BettingInstrument {
313    fn hash<H: Hasher>(&self, state: &mut H) {
314        self.id.hash(state);
315    }
316}
317
318impl Instrument for BettingInstrument {
319    fn into_any(self) -> InstrumentAny {
320        InstrumentAny::Betting(self)
321    }
322
323    fn id(&self) -> InstrumentId {
324        self.id
325    }
326
327    fn raw_symbol(&self) -> Symbol {
328        self.raw_symbol
329    }
330
331    fn asset_class(&self) -> AssetClass {
332        AssetClass::Alternative
333    }
334
335    fn instrument_class(&self) -> InstrumentClass {
336        InstrumentClass::SportsBetting
337    }
338
339    fn underlying(&self) -> Option<Ustr> {
340        None
341    }
342
343    fn quote_currency(&self) -> Currency {
344        self.currency
345    }
346
347    fn base_currency(&self) -> Option<Currency> {
348        None
349    }
350
351    fn settlement_currency(&self) -> Currency {
352        self.currency
353    }
354
355    fn isin(&self) -> Option<Ustr> {
356        None
357    }
358
359    fn exchange(&self) -> Option<Ustr> {
360        None
361    }
362
363    fn option_kind(&self) -> Option<OptionKind> {
364        None
365    }
366
367    fn is_inverse(&self) -> bool {
368        false
369    }
370
371    fn price_precision(&self) -> u8 {
372        self.price_precision
373    }
374
375    fn size_precision(&self) -> u8 {
376        self.size_precision
377    }
378
379    fn price_increment(&self) -> Price {
380        self.price_increment
381    }
382
383    fn size_increment(&self) -> Quantity {
384        self.size_increment
385    }
386
387    fn multiplier(&self) -> Quantity {
388        Quantity::from(1)
389    }
390
391    fn lot_size(&self) -> Option<Quantity> {
392        Some(Quantity::from(1))
393    }
394
395    fn max_quantity(&self) -> Option<Quantity> {
396        self.max_quantity
397    }
398
399    fn min_quantity(&self) -> Option<Quantity> {
400        self.min_quantity
401    }
402
403    fn max_price(&self) -> Option<Price> {
404        self.max_price
405    }
406
407    fn min_price(&self) -> Option<Price> {
408        self.min_price
409    }
410
411    fn ts_event(&self) -> UnixNanos {
412        self.ts_event
413    }
414
415    fn ts_init(&self) -> UnixNanos {
416        self.ts_init
417    }
418
419    fn strike_price(&self) -> Option<Price> {
420        None
421    }
422
423    fn activation_ns(&self) -> Option<UnixNanos> {
424        Some(self.market_start_time)
425    }
426
427    fn expiration_ns(&self) -> Option<UnixNanos> {
428        None
429    }
430
431    fn max_notional(&self) -> Option<Money> {
432        self.max_notional
433    }
434
435    fn min_notional(&self) -> Option<Money> {
436        self.min_notional
437    }
438}
439
440////////////////////////////////////////////////////////////////////////////////
441// Tests
442////////////////////////////////////////////////////////////////////////////////
443#[cfg(test)]
444mod tests {
445    use rstest::rstest;
446
447    use crate::instruments::{BettingInstrument, stubs::*};
448
449    #[rstest]
450    fn test_equality(betting: BettingInstrument) {
451        let cloned = betting;
452        assert_eq!(betting, cloned);
453    }
454}