pub mod account;
pub mod book;
pub mod stamp;
pub use account::{Account, KEEP};
pub use book::{Book, DEFAULT_DEPTH};
pub use stamp::Stamped;
use std::cmp::Reverse;
use std::collections::{BTreeMap, HashSet};
use crate::live::store::stamp::put;
use crate::live::{BoMarketStats, Data, Frame, Height, MarketStats, SpotMarketStats, Subscription, Ticker};
use crate::markets::Trade;
use crate::utils::{AccountIndex, Dec, MarketId};
pub const TAPE: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Folded {
Changed,
Same,
Dropped,
}
impl Folded {
pub(crate) const fn of(changed: bool) -> Folded {
match changed {
true => Folded::Changed,
false => Folded::Same,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Store {
books: BTreeMap<MarketId, Stamped<Book>>,
tickers: BTreeMap<MarketId, Stamped<Ticker>>,
perps: BTreeMap<MarketId, Stamped<MarketStats>>,
spots: BTreeMap<MarketId, Stamped<SpotMarketStats>>,
options: BTreeMap<MarketId, Stamped<BoMarketStats>>,
tapes: BTreeMap<MarketId, Stamped<Vec<Trade>>>,
height: Option<Stamped<Height>>,
accounts: BTreeMap<AccountIndex, Account>,
}
impl Store {
#[must_use]
pub fn new() -> Store {
Store::default()
}
pub fn apply(&mut self, now: u64, frame: &Frame) -> Folded {
if let Frame::Gap(gap) = frame {
let Some(book) = self.books.get_mut(&gap.market) else { return Folded::Dropped };
book.value.reset();
book.touch(now, true);
return Folded::Changed;
}
let Some(data) = frame.data() else { return Folded::Dropped };
let whole = frame.is_snapshot();
match data {
Data::OrderBook(update) if whole => {
let book =
self.books.entry(update.market).or_insert_with(|| Stamped::new(now, Book::new(update.market)));
book.value.snapshot(update);
book.touch(now, true);
Folded::Changed
}
Data::OrderBook(update) => {
let Some(book) = self.books.get_mut(&update.market) else { return Folded::Dropped };
match book.value.update(update) {
Ok(true) | Err(_) => {
book.touch(now, true);
Folded::Changed
}
Ok(false) => Folded::Dropped,
}
}
Data::Ticker(ticker) => fold(&mut self.tickers, now, ticker.market, ticker),
Data::MarketStats(rows) => {
rows.iter().map(|row| fold(&mut self.perps, now, row.market_id, row)).fold(Folded::Dropped, or)
}
Data::SpotMarketStats(rows) => {
rows.iter().map(|row| fold(&mut self.spots, now, row.market_id, row)).fold(Folded::Dropped, or)
}
Data::BoMarketStats(rows) => {
rows.iter().map(|row| fold(&mut self.options, now, row.market_id, row)).fold(Folded::Dropped, or)
}
Data::Trades(trades) => {
let tape = self.tapes.entry(trades.market).or_insert_with(|| Stamped::new(now, Vec::new()));
let changed = fold_prints(&mut tape.value, trades.all());
tape.touch(now, changed);
Folded::of(changed)
}
Data::Height(height) => {
let newer = self.height.as_ref().is_none_or(|held| held.height <= height.height);
match newer {
true => Folded::of(put(&mut self.height, now, *height)),
false => Folded::Dropped,
}
}
_ => match account_of(data) {
Some(index) => {
let folded = self.accounts.entry(index).or_insert_with(|| Account::new(index)).apply(now, frame);
if self.accounts.get(&index) == Some(&Account::new(index)) {
self.accounts.remove(&index);
}
folded
}
None => Folded::Dropped,
},
}
}
#[must_use]
pub fn book(&self, market: MarketId) -> Option<&Stamped<Book>> {
self.books.get(&market)
}
#[must_use]
pub fn ticker(&self, market: MarketId) -> Option<&Stamped<Ticker>> {
self.tickers.get(&market)
}
#[must_use]
pub fn market_stats(&self, market: MarketId) -> Option<&Stamped<MarketStats>> {
self.perps.get(&market)
}
#[must_use]
pub fn spot_stats(&self, market: MarketId) -> Option<&Stamped<SpotMarketStats>> {
self.spots.get(&market)
}
#[must_use]
pub fn option_stats(&self, market: MarketId) -> Option<&Stamped<BoMarketStats>> {
self.options.get(&market)
}
#[must_use]
pub fn tape(&self, market: MarketId) -> Option<&Stamped<Vec<Trade>>> {
self.tapes.get(&market)
}
#[must_use]
pub const fn height(&self) -> Option<&Stamped<Height>> {
self.height.as_ref()
}
#[must_use]
pub fn account(&self, index: AccountIndex) -> Option<&Account> {
self.accounts.get(&index)
}
#[must_use]
pub fn mid(&self, market: MarketId) -> Option<Dec> {
let book = self.books.get(&market).filter(|book| book.is_ready()).and_then(|book| book.mid());
book.or_else(|| self.tickers.get(&market).and_then(|ticker| ticker.mid()))
.or_else(|| self.perps.get(&market).and_then(|stats| stats.mid_price))
.or_else(|| self.spots.get(&market).and_then(|stats| stats.mid_price))
.or_else(|| self.options.get(&market).and_then(|stats| stats.mid_price))
}
pub fn forget(&mut self, subscription: &Subscription) {
match subscription {
Subscription::OrderBook { market } => {
self.books.remove(market);
}
Subscription::Ticker { market } => {
self.tickers.remove(market);
}
Subscription::Trade { market } => {
self.tapes.remove(market);
}
other => {
if let Some(account) = other.account() {
self.accounts.remove(&account);
}
}
}
}
pub fn closed(&mut self) {
for book in self.books.values_mut() {
book.value.reset();
}
}
}
fn account_of(data: &Data) -> Option<AccountIndex> {
Some(match data {
Data::AccountAll(all) => all.account,
Data::AccountMarket(one) => one.account,
Data::UserStats(stats) => stats.account,
Data::AccountTx(txs) => txs.account,
Data::AccountAllOrders(all) => all.account,
Data::AccountOrders(one) => one.account,
Data::AccountAllTrades(all) => all.account,
Data::AccountAllPositions(all) => all.account,
Data::AccountAllAssets(all) => all.account,
_ => return None,
})
}
fn fold<T: Clone + PartialEq>(
slots: &mut BTreeMap<MarketId, Stamped<T>>,
now: u64,
market: MarketId,
value: &T,
) -> Folded {
match slots.get_mut(&market) {
Some(held) => Folded::of(held.set(now, value.clone())),
None => {
slots.insert(market, Stamped::new(now, value.clone()));
Folded::Changed
}
}
}
const fn or(one: Folded, other: Folded) -> Folded {
match (one, other) {
(Folded::Changed, _) | (_, Folded::Changed) => Folded::Changed,
(Folded::Same, _) | (_, Folded::Same) => Folded::Same,
_ => Folded::Dropped,
}
}
fn fold_prints<'a>(tape: &mut Vec<Trade>, prints: impl Iterator<Item = &'a Trade>) -> bool {
let mut seen: HashSet<u64> = tape.iter().map(|print| print.trade_id).collect();
let fresh: Vec<Trade> = prints.filter(|print| seen.insert(print.trade_id)).cloned().collect();
if fresh.is_empty() {
return false;
}
tape.extend(fresh);
tape.sort_by_key(|print| Reverse((print.timestamp, print.trade_id)));
tape.truncate(TAPE);
true
}
#[cfg(test)]
#[path = "tests/store.rs"]
mod tests;