use crate::book::{BinaryPriceSize, BookMarketState, PriceSize};
use crate::types::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SnapshotPriceSelection {
Best,
LiquidityWeighted,
DistanceWeighted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SnapshotPriceSelections {
pub best: Option<OddsX10000>,
pub liquidity_weighted: Option<OddsX10000>,
pub distance_weighted: Option<OddsX10000>,
}
impl SnapshotPriceSelections {
pub fn from_levels(levels: &[PriceSize]) -> Self {
Self {
best: select_price(levels, SnapshotPriceSelection::Best),
liquidity_weighted: select_price(levels, SnapshotPriceSelection::LiquidityWeighted),
distance_weighted: select_price(levels, SnapshotPriceSelection::DistanceWeighted),
}
}
pub fn get(self, mode: SnapshotPriceSelection) -> Option<OddsX10000> {
match mode {
SnapshotPriceSelection::Best => self.best,
SnapshotPriceSelection::LiquidityWeighted => self.liquidity_weighted,
SnapshotPriceSelection::DistanceWeighted => self.distance_weighted,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MarketSnapshot {
pub market_id: MarketId,
pub name: String,
pub market_seq: u64,
pub state: BookMarketState,
pub phase: MarketPhase,
pub metadata: Option<serde_json::Value>,
pub book: MarketBookSnapshot,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MarketBookSnapshot {
ExchangeOdds(ExchangeMarketSnapshot),
BinaryYes(BinaryMarketSnapshot),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExchangeMarketSnapshot {
pub runners: Vec<RunnerSnapshot>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnerRefSnapshot {
pub runner_id: RunnerId,
pub runner_label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryMarketSnapshot {
pub yes_runner: RunnerRefSnapshot,
pub no_runner: RunnerRefSnapshot,
pub max_price_ticks: u16,
pub bids: Vec<BinaryPriceSize>,
pub asks: Vec<BinaryPriceSize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunnerSnapshot {
pub runner_id: RunnerId,
pub runner_label: String,
pub available_to_back: Vec<PriceSize>,
pub available_to_lay: Vec<PriceSize>,
pub matched_volume: Money,
pub selected_to_back: SnapshotPriceSelections,
pub selected_to_lay: SnapshotPriceSelections,
}
impl MarketSnapshot {
pub fn market_model(&self) -> MarketModel {
self.book.market_model()
}
pub fn exchange(&self) -> Option<&ExchangeMarketSnapshot> {
self.book.exchange()
}
pub fn exchange_mut(&mut self) -> Option<&mut ExchangeMarketSnapshot> {
self.book.exchange_mut()
}
pub fn binary(&self) -> Option<&BinaryMarketSnapshot> {
self.book.binary()
}
pub fn binary_mut(&mut self) -> Option<&mut BinaryMarketSnapshot> {
self.book.binary_mut()
}
pub fn runners(&self) -> &[RunnerSnapshot] {
self.exchange().map(|b| b.runners.as_slice()).unwrap_or(&[])
}
pub fn runners_mut(&mut self) -> Option<&mut Vec<RunnerSnapshot>> {
self.exchange_mut().map(|b| &mut b.runners)
}
pub fn runner(&self, runner_id: RunnerId) -> Option<&RunnerSnapshot> {
self.runners().iter().find(|r| r.runner_id == runner_id)
}
pub fn runner_count(&self) -> usize {
self.runners().len()
}
pub fn runner_refs(&self) -> Vec<RunnerRefSnapshot> {
match &self.book {
MarketBookSnapshot::ExchangeOdds(book) => book
.runners
.iter()
.map(|runner| RunnerRefSnapshot {
runner_id: runner.runner_id,
runner_label: runner.runner_label.clone(),
})
.collect(),
MarketBookSnapshot::BinaryYes(book) => {
vec![book.yes_runner.clone(), book.no_runner.clone()]
}
}
}
pub fn runner_ids(&self) -> Vec<RunnerId> {
self.runner_refs()
.into_iter()
.map(|runner| runner.runner_id)
.collect()
}
pub fn truncate_depth(&mut self, depth: usize) {
match &mut self.book {
MarketBookSnapshot::ExchangeOdds(book) => {
for runner in &mut book.runners {
runner.available_to_back.truncate(depth);
runner.available_to_lay.truncate(depth);
}
}
MarketBookSnapshot::BinaryYes(book) => {
book.bids.truncate(depth);
book.asks.truncate(depth);
}
}
}
}
impl MarketBookSnapshot {
pub fn market_model(&self) -> MarketModel {
match self {
Self::ExchangeOdds(_) => MarketModel::ExchangeOdds,
Self::BinaryYes(book) => MarketModel::BinaryYes {
max_price_ticks: book.max_price_ticks,
},
}
}
pub fn exchange(&self) -> Option<&ExchangeMarketSnapshot> {
match self {
Self::ExchangeOdds(book) => Some(book),
Self::BinaryYes(_) => None,
}
}
pub fn exchange_mut(&mut self) -> Option<&mut ExchangeMarketSnapshot> {
match self {
Self::ExchangeOdds(book) => Some(book),
Self::BinaryYes(_) => None,
}
}
pub fn binary(&self) -> Option<&BinaryMarketSnapshot> {
match self {
Self::ExchangeOdds(_) => None,
Self::BinaryYes(book) => Some(book),
}
}
pub fn binary_mut(&mut self) -> Option<&mut BinaryMarketSnapshot> {
match self {
Self::ExchangeOdds(_) => None,
Self::BinaryYes(book) => Some(book),
}
}
}
impl RunnerSnapshot {
pub fn best_back(&self) -> Option<&PriceSize> {
self.available_to_back.first()
}
pub fn best_lay(&self) -> Option<&PriceSize> {
self.available_to_lay.first()
}
pub fn select_back_price(&self, mode: SnapshotPriceSelection) -> Option<OddsX10000> {
self.selected_to_back
.get(mode)
.or_else(|| select_price(&self.available_to_back, mode))
}
pub fn select_lay_price(&self, mode: SnapshotPriceSelection) -> Option<OddsX10000> {
self.selected_to_lay
.get(mode)
.or_else(|| select_price(&self.available_to_lay, mode))
}
pub fn spread_ticks(&self) -> Option<u32> {
let back = self.best_back()?;
let lay = self.best_lay()?;
back.price
.ticks_between(lay.price)
.map(|t| t.unsigned_abs())
}
pub fn total_back_volume(&self) -> Money {
Money(self.available_to_back.iter().map(|p| p.size.0).sum())
}
pub fn total_lay_volume(&self) -> Money {
Money(self.available_to_lay.iter().map(|p| p.size.0).sum())
}
}
fn select_price(levels: &[PriceSize], mode: SnapshotPriceSelection) -> Option<OddsX10000> {
match mode {
SnapshotPriceSelection::Best => levels.first().map(|p| p.price),
SnapshotPriceSelection::LiquidityWeighted => {
weighted_price(levels, |_, level| level.size.0.max(0) as f64)
}
SnapshotPriceSelection::DistanceWeighted => {
let best = levels.first()?.price;
weighted_price(levels, |idx, level| {
let base = level
.price
.ticks_between(best)
.map(|d| d.unsigned_abs() as f64)
.unwrap_or(idx as f64);
1.0 / (base + 1.0)
})
}
}
}
fn weighted_price<F>(levels: &[PriceSize], mut weight_for: F) -> Option<OddsX10000>
where
F: FnMut(usize, &PriceSize) -> f64,
{
let mut weighted_sum = 0.0;
let mut weight_total = 0.0;
for (idx, level) in levels.iter().enumerate() {
let weight = weight_for(idx, level);
if !weight.is_finite() || weight <= 0.0 {
continue;
}
weighted_sum += level.price.to_decimal() * weight;
weight_total += weight;
}
if weight_total <= f64::EPSILON {
return levels.first().map(|p| p.price);
}
nearest_tick(weighted_sum / weight_total)
}
fn nearest_tick(decimal: f64) -> Option<OddsX10000> {
let raw = OddsX10000::from_decimal(decimal)?;
let floor = raw.floor_tick();
let ceil = raw.ceil_tick();
match (floor, ceil) {
(Some(f), Some(c)) => {
let f_diff = (decimal - f.to_decimal()).abs();
let c_diff = (c.to_decimal() - decimal).abs();
if f_diff <= c_diff { Some(f) } else { Some(c) }
}
(Some(f), None) => Some(f),
(None, Some(c)) => Some(c),
(None, None) => None,
}
}