tycho_simulation/rfq/
models.rs1use std::collections::{HashMap, HashSet};
2
3use serde::{Deserialize, Serialize};
4use tycho_client::feed::{BlockHeader, HeaderLike};
5use tycho_common::Bytes;
6
7#[derive(Clone, Default, Debug)]
8pub struct TimestampHeader {
9 pub timestamp: u64,
10}
11
12impl HeaderLike for TimestampHeader {
13 fn block(self) -> Option<BlockHeader> {
14 None
15 }
16
17 fn block_number_or_timestamp(self) -> u64 {
18 self.timestamp
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct PriceLevel {
26 #[serde(
27 rename = "q",
28 deserialize_with = "deserialize_string_to_f64",
29 serialize_with = "serialize_f64_to_string"
30 )]
31 pub quantity: f64,
32 #[serde(
33 rename = "p",
34 deserialize_with = "deserialize_string_to_f64",
35 serialize_with = "serialize_f64_to_string"
36 )]
37 pub price: f64,
38}
39
40fn deserialize_string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
41where
42 D: serde::Deserializer<'de>,
43{
44 let s = String::deserialize(deserializer)?;
45 s.parse()
46 .map_err(serde::de::Error::custom)
47}
48
49fn serialize_f64_to_string<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
50where
51 S: serde::Serializer,
52{
53 serializer.serialize_str(&value.to_string())
54}
55
56pub fn fill_levels(levels: &[PriceLevel], amount_in: f64) -> (f64, f64) {
59 let mut remaining_amount_in = amount_in;
60 let mut amount_out = 0.0;
61 for level in levels {
62 if remaining_amount_in <= 0.0 {
63 break;
64 }
65 let filled = remaining_amount_in.min(level.quantity);
66 amount_out += filled * level.price;
67 remaining_amount_in -= filled;
68 }
69 (amount_out, remaining_amount_in)
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ComponentLayout {
76 #[default]
80 PerPair,
81 AllPairs,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum QuoteRule {
94 OncePerMaker,
97 OncePerVenue,
99}
100
101impl QuoteRule {
102 pub const ATTRIBUTE: &'static str = "quote_rule";
103 const ALL: [QuoteRule; 2] = [QuoteRule::OncePerMaker, QuoteRule::OncePerVenue];
104
105 pub fn as_str(self) -> &'static str {
107 match self {
108 QuoteRule::OncePerMaker => "once_per_maker",
109 QuoteRule::OncePerVenue => "once_per_venue",
110 }
111 }
112
113 pub fn from_attributes(
115 attributes: &HashMap<String, Bytes>,
116 ) -> Result<Option<QuoteRule>, String> {
117 let Some(value) = attributes.get(Self::ATTRIBUTE) else {
118 return Ok(None);
119 };
120 Self::ALL
121 .into_iter()
122 .find(|rule| rule.as_str().as_bytes() == value.as_ref())
123 .map(Some)
124 .ok_or_else(|| {
125 format!("Unknown quote_rule attribute: {}", String::from_utf8_lossy(value))
126 })
127 }
128
129 pub fn allows(self, used: &HashSet<String>, market_maker: &str) -> bool {
131 match self {
132 QuoteRule::OncePerMaker => !used.contains(market_maker),
133 QuoteRule::OncePerVenue => used.is_empty(),
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn fill_levels_stops_when_levels_run_out() {
144 let levels = vec![
145 PriceLevel { quantity: 1.0, price: 3000.0 },
146 PriceLevel { quantity: 2.0, price: 2999.0 },
147 ];
148 assert_eq!(fill_levels(&levels, 1.0), (3000.0, 0.0));
149 assert_eq!(fill_levels(&levels, 2.0), (5999.0, 0.0));
150 assert_eq!(fill_levels(&levels, 5.0), (8998.0, 2.0));
151 }
152
153 #[test]
154 fn quote_rule_attribute_round_trip() {
155 for rule in [QuoteRule::OncePerMaker, QuoteRule::OncePerVenue] {
156 let attributes = HashMap::from([(
157 QuoteRule::ATTRIBUTE.to_string(),
158 rule.as_str().as_bytes().into(),
159 )]);
160 assert_eq!(QuoteRule::from_attributes(&attributes), Ok(Some(rule)));
161 }
162 }
163
164 #[test]
165 fn quote_rule_attribute_absent() {
166 assert_eq!(QuoteRule::from_attributes(&HashMap::new()), Ok(None));
167 }
168
169 #[test]
170 fn quote_rule_attribute_unknown_value() {
171 let attributes =
172 HashMap::from([(QuoteRule::ATTRIBUTE.to_string(), b"twice_per_venue".into())]);
173 let result = QuoteRule::from_attributes(&attributes);
174 assert!(matches!(result, Err(message) if message.contains("twice_per_venue")));
175 }
176}