use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FixedPrice(u64);
impl FixedPrice {
pub const SCALE: u64 = 10_000;
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(Self::SCALE);
#[inline]
pub fn from_f64(price: f64) -> Self {
Self((price * Self::SCALE as f64).round() as u64)
}
#[inline]
pub fn to_f64(self) -> f64 {
self.0 as f64 / Self::SCALE as f64
}
#[inline]
pub fn raw(self) -> u64 {
self.0
}
#[inline]
pub fn from_raw(raw: u64) -> Self {
Self(raw)
}
#[inline]
pub fn complement(self) -> Self {
Self(Self::SCALE.saturating_sub(self.0))
}
#[inline]
pub fn midpoint(self, other: Self) -> Self {
Self((self.0 + other.0) / 2)
}
}
impl std::fmt::Debug for FixedPrice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FixedPrice({})", self.to_f64())
}
}
impl std::fmt::Display for FixedPrice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_f64())
}
}
impl Default for FixedPrice {
fn default() -> Self {
Self::ZERO
}
}
impl From<f64> for FixedPrice {
#[inline]
fn from(v: f64) -> Self {
Self::from_f64(v)
}
}
impl From<FixedPrice> for f64 {
#[inline]
fn from(v: FixedPrice) -> Self {
v.to_f64()
}
}
impl Serialize for FixedPrice {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f64(self.to_f64())
}
}
impl<'de> Deserialize<'de> for FixedPrice {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = f64::deserialize(deserializer)?;
Ok(Self::from_f64(v))
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for FixedPrice {
fn schema_name() -> String {
"number".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
f64::json_schema(gen)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PriceLevelSide {
Bid,
Ask,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PriceLevelChange {
pub side: PriceLevelSide,
pub price: FixedPrice,
pub size: f64,
}
pub type ChangeVec = SmallVec<[PriceLevelChange; 4]>;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PriceLevel {
pub price: FixedPrice,
pub size: f64,
}
impl PriceLevel {
#[inline]
pub fn new(price: f64, size: f64) -> Self {
Self {
price: FixedPrice::from_f64(price),
size,
}
}
#[inline]
pub fn with_fixed(price: FixedPrice, size: f64) -> Self {
Self { price, size }
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Orderbook {
pub market_id: String,
pub asset_id: String,
pub bids: Vec<PriceLevel>,
pub asks: Vec<PriceLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_update_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
}
impl Orderbook {
#[inline]
pub fn best_bid(&self) -> Option<f64> {
self.bids.first().map(|l| l.price.to_f64())
}
#[inline]
pub fn best_ask(&self) -> Option<f64> {
self.asks.first().map(|l| l.price.to_f64())
}
#[inline]
pub fn mid_price(&self) -> Option<f64> {
match (self.bids.first(), self.asks.first()) {
(Some(bid), Some(ask)) => Some(bid.price.midpoint(ask.price).to_f64()),
_ => None,
}
}
#[inline]
pub fn spread(&self) -> Option<f64> {
match (self.bids.first(), self.asks.first()) {
(Some(bid), Some(ask)) => Some(ask.price.to_f64() - bid.price.to_f64()),
_ => None,
}
}
#[inline]
pub fn has_data(&self) -> bool {
!self.bids.is_empty() && !self.asks.is_empty()
}
pub fn sort(&mut self) {
sort_bids(&mut self.bids);
sort_asks(&mut self.asks);
}
pub fn from_rest_response(
bids: &[RestPriceLevel],
asks: &[RestPriceLevel],
asset_id: impl Into<String>,
) -> Self {
let mut parsed_bids: Vec<PriceLevel> = bids
.iter()
.filter_map(|b| {
let price = b.price.parse::<f64>().ok()?;
let size = b.size.parse::<f64>().ok()?;
if price > 0.0 && size > 0.0 {
Some(PriceLevel::new(price, size))
} else {
None
}
})
.collect();
let mut parsed_asks: Vec<PriceLevel> = asks
.iter()
.filter_map(|a| {
let price = a.price.parse::<f64>().ok()?;
let size = a.size.parse::<f64>().ok()?;
if price > 0.0 && size > 0.0 {
Some(PriceLevel::new(price, size))
} else {
None
}
})
.collect();
sort_bids(&mut parsed_bids);
sort_asks(&mut parsed_asks);
Self {
market_id: String::new(),
asset_id: asset_id.into(),
bids: parsed_bids,
asks: parsed_asks,
last_update_id: None,
timestamp: Some(Utc::now()),
hash: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct OrderbookSnapshot {
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recorded_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
pub bids: Vec<PriceLevel>,
pub asks: Vec<PriceLevel>,
}
pub fn sort_bids(levels: &mut [PriceLevel]) {
levels.sort_unstable_by_key(|l| std::cmp::Reverse(l.price));
}
pub fn sort_asks(levels: &mut [PriceLevel]) {
levels.sort_unstable_by_key(|l| l.price);
}
#[inline]
pub fn insert_bid(levels: &mut Vec<PriceLevel>, level: PriceLevel) {
let idx = levels.partition_point(|l| l.price > level.price);
levels.insert(idx, level);
}
#[inline]
pub fn insert_ask(levels: &mut Vec<PriceLevel>, level: PriceLevel) {
let idx = levels.partition_point(|l| l.price < level.price);
levels.insert(idx, level);
}
pub fn apply_bid_level(levels: &mut Vec<PriceLevel>, level: PriceLevel) {
match levels.binary_search_by(|l| level.price.cmp(&l.price)) {
Ok(idx) => {
if level.size > 0.0 {
levels[idx] = level;
} else {
levels.remove(idx);
}
}
Err(idx) => {
if level.size > 0.0 {
levels.insert(idx, level);
}
}
}
}
pub fn apply_ask_level(levels: &mut Vec<PriceLevel>, level: PriceLevel) {
match levels.binary_search_by(|l| l.price.cmp(&level.price)) {
Ok(idx) => {
if level.size > 0.0 {
levels[idx] = level;
} else {
levels.remove(idx);
}
}
Err(idx) => {
if level.size > 0.0 {
levels.insert(idx, level);
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RestPriceLevel {
pub price: String,
pub size: String,
}