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