nautilus_model/instruments/
betting.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::hash::{Hash, Hasher};
17
18use nautilus_core::{
19    correctness::{check_equal_u8, FAILED},
20    UnixNanos,
21};
22use rust_decimal::Decimal;
23use rust_decimal_macros::dec;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{any::InstrumentAny, Instrument};
28use crate::{
29    enums::{AssetClass, InstrumentClass, OptionKind},
30    identifiers::{InstrumentId, Symbol},
31    types::{
32        currency::Currency,
33        money::Money,
34        price::{check_positive_price, Price},
35        quantity::{check_positive_quantity, 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 = "nautilus_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    #[allow(clippy::too_many_arguments)]
126    pub fn new_checked(
127        id: InstrumentId,
128        raw_symbol: Symbol,
129        event_type_id: u64,
130        event_type_name: Ustr,
131        competition_id: u64,
132        competition_name: Ustr,
133        event_id: u64,
134        event_name: Ustr,
135        event_country_code: Ustr,
136        event_open_date: UnixNanos,
137        betting_type: Ustr,
138        market_id: Ustr,
139        market_name: Ustr,
140        market_type: Ustr,
141        market_start_time: UnixNanos,
142        selection_id: u64,
143        selection_name: Ustr,
144        selection_handicap: f64,
145        currency: Currency,
146        price_precision: u8,
147        size_precision: u8,
148        price_increment: Price,
149        size_increment: Quantity,
150        max_quantity: Option<Quantity>,
151        min_quantity: Option<Quantity>,
152        max_notional: Option<Money>,
153        min_notional: Option<Money>,
154        max_price: Option<Price>,
155        min_price: Option<Price>,
156        margin_init: Option<Decimal>,
157        margin_maint: Option<Decimal>,
158        maker_fee: Option<Decimal>,
159        taker_fee: Option<Decimal>,
160        ts_event: UnixNanos,
161        ts_init: UnixNanos,
162    ) -> anyhow::Result<Self> {
163        check_equal_u8(
164            price_precision,
165            price_increment.precision,
166            stringify!(price_precision),
167            stringify!(price_increment.precision),
168        )?;
169        check_equal_u8(
170            size_precision,
171            size_increment.precision,
172            stringify!(size_precision),
173            stringify!(size_increment.precision),
174        )?;
175        check_positive_price(price_increment.raw, stringify!(price_increment.raw))?;
176        check_positive_quantity(size_increment.raw, stringify!(size_increment.raw))?;
177
178        Ok(Self {
179            id,
180            raw_symbol,
181            event_type_id,
182            event_type_name,
183            competition_id,
184            competition_name,
185            event_id,
186            event_name,
187            event_country_code,
188            event_open_date,
189            betting_type,
190            market_id,
191            market_name,
192            market_type,
193            market_start_time,
194            selection_id,
195            selection_name,
196            selection_handicap,
197            currency,
198            price_precision,
199            size_precision,
200            price_increment,
201            size_increment,
202            max_quantity,
203            min_quantity,
204            max_notional,
205            min_notional,
206            max_price,
207            min_price,
208            margin_init: margin_init.unwrap_or(dec!(1)),
209            margin_maint: margin_maint.unwrap_or(dec!(1)),
210            maker_fee: maker_fee.unwrap_or_default(),
211            taker_fee: taker_fee.unwrap_or_default(),
212            ts_event,
213            ts_init,
214        })
215    }
216
217    /// Creates a new [`BettingInstrument`] instance.
218    #[allow(clippy::too_many_arguments)]
219    pub fn new(
220        id: InstrumentId,
221        raw_symbol: Symbol,
222        event_type_id: u64,
223        event_type_name: Ustr,
224        competition_id: u64,
225        competition_name: Ustr,
226        event_id: u64,
227        event_name: Ustr,
228        event_country_code: Ustr,
229        event_open_date: UnixNanos,
230        betting_type: Ustr,
231        market_id: Ustr,
232        market_name: Ustr,
233        market_type: Ustr,
234        market_start_time: UnixNanos,
235        selection_id: u64,
236        selection_name: Ustr,
237        selection_handicap: f64,
238        currency: Currency,
239        price_precision: u8,
240        size_precision: u8,
241        price_increment: Price,
242        size_increment: Quantity,
243        max_quantity: Option<Quantity>,
244        min_quantity: Option<Quantity>,
245        max_notional: Option<Money>,
246        min_notional: Option<Money>,
247        max_price: Option<Price>,
248        min_price: Option<Price>,
249        margin_init: Option<Decimal>,
250        margin_maint: Option<Decimal>,
251        maker_fee: Option<Decimal>,
252        taker_fee: Option<Decimal>,
253        ts_event: UnixNanos,
254        ts_init: UnixNanos,
255    ) -> Self {
256        Self::new_checked(
257            id,
258            raw_symbol,
259            event_type_id,
260            event_type_name,
261            competition_id,
262            competition_name,
263            event_id,
264            event_name,
265            event_country_code,
266            event_open_date,
267            betting_type,
268            market_id,
269            market_name,
270            market_type,
271            market_start_time,
272            selection_id,
273            selection_name,
274            selection_handicap,
275            currency,
276            price_precision,
277            size_precision,
278            price_increment,
279            size_increment,
280            max_quantity,
281            min_quantity,
282            max_notional,
283            min_notional,
284            max_price,
285            min_price,
286            margin_init,
287            margin_maint,
288            maker_fee,
289            taker_fee,
290            ts_event,
291            ts_init,
292        )
293        .expect(FAILED)
294    }
295}
296
297impl PartialEq<Self> for BettingInstrument {
298    fn eq(&self, other: &Self) -> bool {
299        self.id == other.id
300    }
301}
302
303impl Eq for BettingInstrument {}
304
305impl Hash for BettingInstrument {
306    fn hash<H: Hasher>(&self, state: &mut H) {
307        self.id.hash(state);
308    }
309}
310
311impl Instrument for BettingInstrument {
312    fn into_any(self) -> InstrumentAny {
313        InstrumentAny::Betting(self)
314    }
315
316    fn id(&self) -> InstrumentId {
317        self.id
318    }
319
320    fn raw_symbol(&self) -> Symbol {
321        self.raw_symbol
322    }
323
324    fn asset_class(&self) -> AssetClass {
325        AssetClass::Alternative
326    }
327
328    fn instrument_class(&self) -> InstrumentClass {
329        InstrumentClass::SportsBetting
330    }
331
332    fn underlying(&self) -> Option<Ustr> {
333        None
334    }
335
336    fn quote_currency(&self) -> Currency {
337        self.currency
338    }
339
340    fn base_currency(&self) -> Option<Currency> {
341        None
342    }
343
344    fn settlement_currency(&self) -> Currency {
345        self.currency
346    }
347
348    fn isin(&self) -> Option<Ustr> {
349        None
350    }
351
352    fn exchange(&self) -> Option<Ustr> {
353        None
354    }
355
356    fn option_kind(&self) -> Option<OptionKind> {
357        None
358    }
359
360    fn is_inverse(&self) -> bool {
361        false
362    }
363
364    fn price_precision(&self) -> u8 {
365        self.price_precision
366    }
367
368    fn size_precision(&self) -> u8 {
369        self.size_precision
370    }
371
372    fn price_increment(&self) -> Price {
373        self.price_increment
374    }
375
376    fn size_increment(&self) -> Quantity {
377        self.size_increment
378    }
379
380    fn multiplier(&self) -> Quantity {
381        Quantity::from(1)
382    }
383
384    fn lot_size(&self) -> Option<Quantity> {
385        Some(Quantity::from(1))
386    }
387
388    fn max_quantity(&self) -> Option<Quantity> {
389        self.max_quantity
390    }
391
392    fn min_quantity(&self) -> Option<Quantity> {
393        self.min_quantity
394    }
395
396    fn max_price(&self) -> Option<Price> {
397        self.max_price
398    }
399
400    fn min_price(&self) -> Option<Price> {
401        self.min_price
402    }
403
404    fn ts_event(&self) -> UnixNanos {
405        self.ts_event
406    }
407
408    fn ts_init(&self) -> UnixNanos {
409        self.ts_init
410    }
411
412    fn strike_price(&self) -> Option<Price> {
413        None
414    }
415
416    fn activation_ns(&self) -> Option<UnixNanos> {
417        Some(self.market_start_time)
418    }
419
420    fn expiration_ns(&self) -> Option<UnixNanos> {
421        None
422    }
423
424    fn max_notional(&self) -> Option<Money> {
425        self.max_notional
426    }
427
428    fn min_notional(&self) -> Option<Money> {
429        self.min_notional
430    }
431}
432
433////////////////////////////////////////////////////////////////////////////////
434// Tests
435////////////////////////////////////////////////////////////////////////////////
436#[cfg(test)]
437mod tests {
438    use rstest::rstest;
439
440    use crate::instruments::{stubs::*, BettingInstrument};
441
442    #[rstest]
443    fn test_equality(betting: BettingInstrument) {
444        let cloned = betting;
445        assert_eq!(betting, cloned);
446    }
447}