Skip to main content

nautilus_betfair/common/
types.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
16//! Common type aliases for Betfair identifiers and values.
17
18use nautilus_core::serialization::deserialize_decimal_native;
19use nautilus_model::identifiers::{ClientOrderId, StrategyId};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Deserializer};
22use serde_json;
23
24/// Betfair market identifier (e.g., "1.201070830").
25pub type MarketId = String;
26
27/// Betfair selection (runner) identifier.
28pub type SelectionId = u64;
29
30/// Deserializes a `SelectionId` from either a JSON number or string.
31///
32/// The streaming API sometimes sends selection IDs as strings (e.g. `"19248890"`)
33/// rather than bare integers.
34///
35/// # Errors
36///
37/// Returns an error if the value is a string that cannot be parsed as `u64`.
38pub fn deserialize_selection_id<'de, D>(deserializer: D) -> Result<SelectionId, D::Error>
39where
40    D: Deserializer<'de>,
41{
42    #[derive(Deserialize)]
43    #[serde(untagged)]
44    enum StringOrU64 {
45        U64(u64),
46        Str(String),
47    }
48
49    match StringOrU64::deserialize(deserializer)? {
50        StringOrU64::U64(v) => Ok(v),
51        StringOrU64::Str(s) => s.parse().map_err(serde::de::Error::custom),
52    }
53}
54
55/// Betfair bet identifier.
56pub type BetId = String;
57
58/// Betfair event identifier.
59pub type EventId = String;
60
61/// Betfair event type identifier.
62pub type EventTypeId = String;
63
64/// Betfair exchange identifier.
65pub type ExchangeId = String;
66
67/// Competition identifier.
68pub type CompetitionId = String;
69
70/// Customer order reference (max 32 characters).
71pub type CustomerOrderRef = String;
72
73/// Customer strategy reference (max 15 characters).
74pub type CustomerStrategyRef = String;
75
76/// Handicap value for Asian handicap markets.
77pub type Handicap = Decimal;
78
79#[derive(Deserialize)]
80pub(crate) struct JsonDecimal(
81    #[serde(deserialize_with = "deserialize_decimal_native")] pub Decimal,
82);
83
84pub(crate) fn deserialize_optional_decimal_native<'de, D: Deserializer<'de>>(
85    deserializer: D,
86) -> Result<Option<Decimal>, D::Error> {
87    Option::<JsonDecimal>::deserialize(deserializer).map(|value| value.map(|value| value.0))
88}
89
90/// Cached order snapshot fed into `OcmState::sync_from_orders`.
91#[derive(Debug, Clone)]
92pub struct OrderSyncEntry {
93    pub bet_id: String,
94    pub venue_order_ids: Vec<String>,
95    pub client_order_id: ClientOrderId,
96    pub strategy_id: StrategyId,
97    pub filled_qty: Decimal,
98    pub avg_px: Decimal,
99    pub is_closed: bool,
100    pub trade_ids: Vec<String>,
101}
102
103/// Deserializes an `Option<String>` from either a JSON string or number.
104///
105/// Betfair API docs define many ID fields as strings, but the actual responses
106/// sometimes send them as bare integers (e.g. `"id": 7` instead of `"id": "7"`).
107///
108/// # Errors
109///
110/// Returns an error if the underlying JSON value cannot be deserialized.
111pub fn deserialize_optional_string_lenient<'de, D>(
112    deserializer: D,
113) -> Result<Option<String>, D::Error>
114where
115    D: Deserializer<'de>,
116{
117    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
118    Ok(value.map(|v| match v {
119        serde_json::Value::String(s) => s,
120        serde_json::Value::Number(n) => n.to_string(),
121        other => other.to_string(),
122    }))
123}
124
125/// Deserializes an `Option<u32>` leniently from a JSON number, numeric string, or
126/// empty string.
127///
128/// Betfair navigation responses sometimes send `"numberOfWinners": ""` for
129/// handicap/total-points markets, which would fail a strict `Option<u32>` parse.
130///
131/// # Errors
132///
133/// Returns an error if the value is a non-empty string that cannot be parsed as `u32`.
134pub fn deserialize_optional_u32_lenient<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
135where
136    D: Deserializer<'de>,
137{
138    #[derive(Deserialize)]
139    #[serde(untagged)]
140    enum Value {
141        Num(u32),
142        Str(String),
143    }
144
145    match Option::<Value>::deserialize(deserializer)? {
146        None => Ok(None),
147        Some(Value::Num(n)) => Ok(Some(n)),
148        Some(Value::Str(s)) if s.is_empty() => Ok(None),
149        Some(Value::Str(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
150    }
151}