1use crate::error::PredictionError;
4use crate::instrument::{BinaryMarketKey, OutcomeInstrument};
5use crate::price::{NonZeroContractQuantity, OutcomePrice, PriceGrid};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::{cmp::Reverse, num::NonZeroU32};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum BinaryOutcome {
14 Yes,
16 No,
18}
19
20impl BinaryOutcome {
21 #[must_use]
23 pub const fn complement(self) -> Self {
24 match self {
25 Self::Yes => Self::No,
26 Self::No => Self::Yes,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum BookSide {
35 Bid,
37 Ask,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum TradeAction {
45 Buy,
47 Sell,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub struct BinaryOrderDirection {
54 pub action: TradeAction,
56 pub outcome: BinaryOutcome,
58}
59
60impl BinaryOrderDirection {
61 #[must_use]
63 pub const fn buy_yes() -> Self {
64 Self {
65 action: TradeAction::Buy,
66 outcome: BinaryOutcome::Yes,
67 }
68 }
69
70 #[must_use]
72 pub const fn sell_yes() -> Self {
73 Self {
74 action: TradeAction::Sell,
75 outcome: BinaryOutcome::Yes,
76 }
77 }
78
79 #[must_use]
81 pub const fn buy_no() -> Self {
82 Self {
83 action: TradeAction::Buy,
84 outcome: BinaryOutcome::No,
85 }
86 }
87
88 #[must_use]
90 pub const fn sell_no() -> Self {
91 Self {
92 action: TradeAction::Sell,
93 outcome: BinaryOutcome::No,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
100pub struct PredictionBookLevel {
101 pub quantity: NonZeroContractQuantity,
103 pub price: OutcomePrice,
105 pub order_count: Option<NonZeroU32>,
107}
108
109impl PredictionBookLevel {
110 #[must_use]
112 pub const fn new(price: OutcomePrice, quantity: NonZeroContractQuantity) -> Self {
113 Self {
114 quantity,
115 price,
116 order_count: None,
117 }
118 }
119
120 #[must_use]
122 pub const fn complement_price(self) -> Self {
123 Self {
124 quantity: self.quantity,
125 price: self.price.complement(),
126 order_count: self.order_count,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct PredictionQuoteLevel {
137 pub price: OutcomePrice,
139 #[serde(default)]
141 pub quantity: Option<NonZeroContractQuantity>,
142}
143
144impl PredictionQuoteLevel {
145 #[must_use]
147 pub const fn new(price: OutcomePrice, quantity: Option<NonZeroContractQuantity>) -> Self {
148 Self { price, quantity }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct GenericBinaryQuote<M = ()> {
155 pub market: BinaryMarketKey,
157 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
159 pub as_of: Option<DateTime<Utc>>,
160 pub yes_bid: Option<PredictionQuoteLevel>,
162 pub yes_ask: Option<PredictionQuoteLevel>,
164 pub last_price: Option<OutcomePrice>,
166 #[serde(flatten, default = "Default::default")]
168 pub provider: M,
169}
170
171pub type BinaryQuote = GenericBinaryQuote<()>;
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct GenericBinaryOrderBook<M = ()> {
177 pub market: BinaryMarketKey,
179 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
181 pub as_of: Option<DateTime<Utc>>,
182 pub yes_bids: Vec<PredictionBookLevel>,
184 pub yes_asks: Vec<PredictionBookLevel>,
186 pub price_grid: Option<PriceGrid>,
188 #[serde(flatten, default = "Default::default")]
190 pub provider: M,
191}
192
193impl<M: Default> GenericBinaryOrderBook<M> {
194 #[must_use]
196 pub fn new(market: BinaryMarketKey) -> Self {
197 Self {
198 market,
199 as_of: None,
200 yes_bids: Vec::new(),
201 yes_asks: Vec::new(),
202 price_grid: None,
203 provider: M::default(),
204 }
205 }
206}
207
208impl<M> GenericBinaryOrderBook<M> {
209 pub fn sort_levels(&mut self) {
214 self.yes_bids.sort_by_key(|level| Reverse(level.price));
215 self.yes_asks.sort_by_key(|level| level.price);
216 }
217
218 #[must_use]
220 pub fn best_yes_bid(&self) -> Option<&PredictionBookLevel> {
221 self.yes_bids.first()
222 }
223
224 #[must_use]
226 pub fn best_yes_ask(&self) -> Option<&PredictionBookLevel> {
227 self.yes_asks.first()
228 }
229
230 #[must_use]
232 pub fn best_no_bid(&self) -> Option<PredictionBookLevel> {
233 self.best_yes_ask()
234 .copied()
235 .map(PredictionBookLevel::complement_price)
236 }
237
238 #[must_use]
240 pub fn best_no_ask(&self) -> Option<PredictionBookLevel> {
241 self.best_yes_bid()
242 .copied()
243 .map(PredictionBookLevel::complement_price)
244 }
245
246 #[must_use]
248 pub fn yes_midpoint(&self) -> Option<OutcomePrice> {
249 Some(
250 self.best_yes_bid()?
251 .price
252 .midpoint(self.best_yes_ask()?.price),
253 )
254 }
255
256 #[must_use]
258 pub fn yes_spread(&self) -> Option<OutcomePrice> {
259 let bid = self.best_yes_bid()?.price.micros();
260 let ask = self.best_yes_ask()?.price.micros();
261 let spread = ask.checked_sub(bid)?;
262 Some(OutcomePrice::from_valid_micros(spread))
263 }
264
265 #[must_use]
268 pub fn is_sorted(&self) -> bool {
269 descending_prices(&self.yes_bids) && ascending_prices(&self.yes_asks) && !self.is_crossed()
270 }
271
272 pub fn validate_sorted(&self) -> Result<(), PredictionError> {
280 if !descending_prices(&self.yes_bids) || !ascending_prices(&self.yes_asks) {
281 return Err(PredictionError::UnsortedOrderBook { book: "binary" });
282 }
283
284 if let (Some(bid), Some(ask)) = (self.best_yes_bid(), self.best_yes_ask())
285 && bid.price > ask.price
286 {
287 return Err(PredictionError::CrossedOrderBook {
288 book: "binary",
289 bid_micros: bid.price.micros(),
290 ask_micros: ask.price.micros(),
291 });
292 }
293
294 Ok(())
295 }
296
297 pub fn validate_on_grid(&self) -> Result<(), PredictionError> {
305 let Some(grid) = &self.price_grid else {
306 return Ok(());
307 };
308 grid.validate()?;
309 validate_levels_on_grid(grid, &self.yes_bids)?;
310 validate_levels_on_grid(grid, &self.yes_asks)
311 }
312
313 fn is_crossed(&self) -> bool {
314 matches!(
315 (self.best_yes_bid(), self.best_yes_ask()),
316 (Some(bid), Some(ask)) if bid.price > ask.price
317 )
318 }
319}
320
321pub type BinaryOrderBook = GenericBinaryOrderBook<()>;
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct GenericOutcomeOrderBook<M = ()> {
327 pub instrument: OutcomeInstrument,
329 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
331 pub as_of: Option<DateTime<Utc>>,
332 pub bids: Vec<PredictionBookLevel>,
334 pub asks: Vec<PredictionBookLevel>,
336 pub price_grid: Option<PriceGrid>,
338 #[serde(flatten, default = "Default::default")]
340 pub provider: M,
341}
342
343impl<M: Default> GenericOutcomeOrderBook<M> {
344 #[must_use]
346 pub fn new(instrument: OutcomeInstrument) -> Self {
347 Self {
348 instrument,
349 as_of: None,
350 bids: Vec::new(),
351 asks: Vec::new(),
352 price_grid: None,
353 provider: M::default(),
354 }
355 }
356}
357
358impl<M> GenericOutcomeOrderBook<M> {
359 pub fn sort_levels(&mut self) {
364 self.bids.sort_by_key(|level| Reverse(level.price));
365 self.asks.sort_by_key(|level| level.price);
366 }
367
368 #[must_use]
370 pub fn best_bid(&self) -> Option<&PredictionBookLevel> {
371 self.bids.first()
372 }
373
374 #[must_use]
376 pub fn best_ask(&self) -> Option<&PredictionBookLevel> {
377 self.asks.first()
378 }
379
380 #[must_use]
383 pub fn is_sorted(&self) -> bool {
384 descending_prices(&self.bids) && ascending_prices(&self.asks) && !self.is_crossed()
385 }
386
387 pub fn validate_sorted(&self) -> Result<(), PredictionError> {
395 if !descending_prices(&self.bids) || !ascending_prices(&self.asks) {
396 return Err(PredictionError::UnsortedOrderBook { book: "outcome" });
397 }
398
399 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask())
400 && bid.price > ask.price
401 {
402 return Err(PredictionError::CrossedOrderBook {
403 book: "outcome",
404 bid_micros: bid.price.micros(),
405 ask_micros: ask.price.micros(),
406 });
407 }
408
409 Ok(())
410 }
411
412 pub fn validate_on_grid(&self) -> Result<(), PredictionError> {
420 let Some(grid) = &self.price_grid else {
421 return Ok(());
422 };
423 grid.validate()?;
424 validate_levels_on_grid(grid, &self.bids)?;
425 validate_levels_on_grid(grid, &self.asks)
426 }
427
428 fn is_crossed(&self) -> bool {
429 matches!(
430 (self.best_bid(), self.best_ask()),
431 (Some(bid), Some(ask)) if bid.price > ask.price
432 )
433 }
434}
435
436pub type OutcomeOrderBook = GenericOutcomeOrderBook<()>;
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
441pub struct GenericPredictionTrade<M = ()> {
442 pub instrument: OutcomeInstrument,
444 pub price: OutcomePrice,
446 pub quantity: NonZeroContractQuantity,
448 pub action: Option<TradeAction>,
450 pub trade_id: Option<String>,
452 #[serde(with = "chrono::serde::ts_milliseconds")]
454 pub ts: DateTime<Utc>,
455 #[serde(flatten, default = "Default::default")]
457 pub provider: M,
458}
459
460pub type PredictionTrade = GenericPredictionTrade<()>;
462
463fn descending_prices(levels: &[PredictionBookLevel]) -> bool {
464 levels.windows(2).all(|pair| pair[0].price >= pair[1].price)
465}
466
467fn ascending_prices(levels: &[PredictionBookLevel]) -> bool {
468 levels.windows(2).all(|pair| pair[0].price <= pair[1].price)
469}
470
471fn validate_levels_on_grid(
472 grid: &PriceGrid,
473 levels: &[PredictionBookLevel],
474) -> Result<(), PredictionError> {
475 for level in levels {
476 grid.validate_price(level.price)?;
477 }
478
479 Ok(())
480}