use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use tycho_client::feed::{BlockHeader, HeaderLike};
use tycho_common::Bytes;
#[derive(Clone, Default, Debug)]
pub struct TimestampHeader {
pub timestamp: u64,
}
impl HeaderLike for TimestampHeader {
fn block(self) -> Option<BlockHeader> {
None
}
fn block_number_or_timestamp(self) -> u64 {
self.timestamp
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PriceLevel {
#[serde(
rename = "q",
deserialize_with = "deserialize_string_to_f64",
serialize_with = "serialize_f64_to_string"
)]
pub quantity: f64,
#[serde(
rename = "p",
deserialize_with = "deserialize_string_to_f64",
serialize_with = "serialize_f64_to_string"
)]
pub price: f64,
}
fn deserialize_string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse()
.map_err(serde::de::Error::custom)
}
fn serialize_f64_to_string<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&value.to_string())
}
pub fn fill_levels(levels: &[PriceLevel], amount_in: f64) -> (f64, f64) {
let mut remaining_amount_in = amount_in;
let mut amount_out = 0.0;
for level in levels {
if remaining_amount_in <= 0.0 {
break;
}
let filled = remaining_amount_in.min(level.quantity);
amount_out += filled * level.price;
remaining_amount_in -= filled;
}
(amount_out, remaining_amount_in)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComponentLayout {
#[default]
PerPair,
AllPairs,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuoteRule {
OncePerMaker,
OncePerVenue,
}
impl QuoteRule {
pub const ATTRIBUTE: &'static str = "quote_rule";
const ALL: [QuoteRule; 2] = [QuoteRule::OncePerMaker, QuoteRule::OncePerVenue];
pub fn as_str(self) -> &'static str {
match self {
QuoteRule::OncePerMaker => "once_per_maker",
QuoteRule::OncePerVenue => "once_per_venue",
}
}
pub fn from_attributes(
attributes: &HashMap<String, Bytes>,
) -> Result<Option<QuoteRule>, String> {
let Some(value) = attributes.get(Self::ATTRIBUTE) else {
return Ok(None);
};
Self::ALL
.into_iter()
.find(|rule| rule.as_str().as_bytes() == value.as_ref())
.map(Some)
.ok_or_else(|| {
format!("Unknown quote_rule attribute: {}", String::from_utf8_lossy(value))
})
}
pub fn allows(self, used: &HashSet<String>, market_maker: &str) -> bool {
match self {
QuoteRule::OncePerMaker => !used.contains(market_maker),
QuoteRule::OncePerVenue => used.is_empty(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_levels_stops_when_levels_run_out() {
let levels = vec![
PriceLevel { quantity: 1.0, price: 3000.0 },
PriceLevel { quantity: 2.0, price: 2999.0 },
];
assert_eq!(fill_levels(&levels, 1.0), (3000.0, 0.0));
assert_eq!(fill_levels(&levels, 2.0), (5999.0, 0.0));
assert_eq!(fill_levels(&levels, 5.0), (8998.0, 2.0));
}
#[test]
fn quote_rule_attribute_round_trip() {
for rule in [QuoteRule::OncePerMaker, QuoteRule::OncePerVenue] {
let attributes = HashMap::from([(
QuoteRule::ATTRIBUTE.to_string(),
rule.as_str().as_bytes().into(),
)]);
assert_eq!(QuoteRule::from_attributes(&attributes), Ok(Some(rule)));
}
}
#[test]
fn quote_rule_attribute_absent() {
assert_eq!(QuoteRule::from_attributes(&HashMap::new()), Ok(None));
}
#[test]
fn quote_rule_attribute_unknown_value() {
let attributes =
HashMap::from([(QuoteRule::ATTRIBUTE.to_string(), b"twice_per_venue".into())]);
let result = QuoteRule::from_attributes(&attributes);
assert!(matches!(result, Err(message) if message.contains("twice_per_venue")));
}
}