1use crate::types::{NanoTimestamp, Price, Quantity, Side, Symbol};
17use rust_decimal::Decimal;
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct Tick {
22 pub symbol: Symbol,
24 pub price: Price,
26 pub quantity: Quantity,
28 pub side: Side,
30 pub timestamp: NanoTimestamp,
32}
33
34impl Tick {
35 pub fn new(
44 symbol: Symbol,
45 price: Price,
46 quantity: Quantity,
47 side: Side,
48 timestamp: NanoTimestamp,
49 ) -> Self {
50 Self {
51 symbol,
52 price,
53 quantity,
54 side,
55 timestamp,
56 }
57 }
58
59 pub fn notional(&self) -> Decimal {
64 self.price.value() * self.quantity.value()
65 }
66}
67
68pub struct TickFilter {
72 symbol: Option<Symbol>,
73 side: Option<Side>,
74 min_qty: Option<Quantity>,
75}
76
77impl TickFilter {
78 pub fn new() -> Self {
80 Self {
81 symbol: None,
82 side: None,
83 min_qty: None,
84 }
85 }
86
87 #[must_use]
89 pub fn symbol(mut self, s: Symbol) -> Self {
90 self.symbol = Some(s);
91 self
92 }
93
94 #[must_use]
96 pub fn side(mut self, s: Side) -> Self {
97 self.side = Some(s);
98 self
99 }
100
101 #[must_use]
103 pub fn min_quantity(mut self, q: Quantity) -> Self {
104 self.min_qty = Some(q);
105 self
106 }
107
108 pub fn matches(&self, tick: &Tick) -> bool {
113 if let Some(ref sym) = self.symbol {
114 if tick.symbol != *sym {
115 return false;
116 }
117 }
118 if let Some(ref side) = self.side {
119 if tick.side != *side {
120 return false;
121 }
122 }
123 if let Some(ref min_qty) = self.min_qty {
124 if tick.quantity < *min_qty {
125 return false;
126 }
127 }
128 true
129 }
130}
131
132impl Default for TickFilter {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138pub struct TickReplayer {
140 ticks: Vec<Tick>,
141 index: usize,
142}
143
144impl TickReplayer {
145 pub fn new(mut ticks: Vec<Tick>) -> Self {
147 ticks.sort_by_key(|t| t.timestamp);
148 Self { ticks, index: 0 }
149 }
150
151 pub fn next_tick(&mut self) -> Option<&Tick> {
153 let tick = self.ticks.get(self.index)?;
154 self.index += 1;
155 Some(tick)
156 }
157
158 pub fn remaining(&self) -> usize {
160 self.ticks.len().saturating_sub(self.index)
161 }
162
163 pub fn reset(&mut self) {
165 self.index = 0;
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use rust_decimal_macros::dec;
173
174 fn make_tick(sym: &str, price: &str, qty: &str, side: Side, ts: i64) -> Tick {
175 Tick::new(
176 Symbol::new(sym).unwrap(),
177 Price::new(dec_from_str(price)).unwrap(),
178 Quantity::new(dec_from_str(qty)).unwrap(),
179 side,
180 NanoTimestamp(ts),
181 )
182 }
183
184 fn dec_from_str(s: &str) -> Decimal {
185 s.parse().unwrap()
186 }
187
188 #[test]
189 fn test_tick_notional_is_price_times_quantity() {
190 let t = make_tick("AAPL", "150.00", "10", Side::Ask, 0);
191 assert_eq!(t.notional(), dec!(1500.00));
192 }
193
194 #[test]
195 fn test_tick_notional_zero_quantity() {
196 let t = make_tick("AAPL", "150.00", "0", Side::Ask, 0);
197 assert_eq!(t.notional(), dec!(0));
198 }
199
200 #[test]
201 fn test_tick_filter_no_predicates_matches_all() {
202 let f = TickFilter::new();
203 let t = make_tick("AAPL", "1", "1", Side::Bid, 0);
204 assert!(f.matches(&t));
205 }
206
207 #[test]
208 fn test_tick_filter_by_symbol() {
209 let sym = Symbol::new("AAPL").unwrap();
210 let f = TickFilter::new().symbol(sym);
211 let matching = make_tick("AAPL", "1", "1", Side::Bid, 0);
212 let non_matching = make_tick("TSLA", "1", "1", Side::Bid, 0);
213 assert!(f.matches(&matching));
214 assert!(!f.matches(&non_matching));
215 }
216
217 #[test]
218 fn test_tick_filter_by_side() {
219 let f = TickFilter::new().side(Side::Ask);
220 let ask_tick = make_tick("AAPL", "1", "1", Side::Ask, 0);
221 let bid_tick = make_tick("AAPL", "1", "1", Side::Bid, 0);
222 assert!(f.matches(&ask_tick));
223 assert!(!f.matches(&bid_tick));
224 }
225
226 #[test]
227 fn test_tick_filter_by_min_quantity() {
228 let min_qty = Quantity::new(dec!(5)).unwrap();
229 let f = TickFilter::new().min_quantity(min_qty);
230 let large = make_tick("AAPL", "1", "10", Side::Bid, 0);
231 let small = make_tick("AAPL", "1", "2", Side::Bid, 0);
232 assert!(f.matches(&large));
233 assert!(!f.matches(&small));
234 }
235
236 #[test]
237 fn test_tick_filter_combined_predicates() {
238 let sym = Symbol::new("AAPL").unwrap();
239 let min_qty = Quantity::new(dec!(5)).unwrap();
240 let f = TickFilter::new()
241 .symbol(sym)
242 .side(Side::Bid)
243 .min_quantity(min_qty);
244 let ok = make_tick("AAPL", "1", "10", Side::Bid, 0);
245 let wrong_sym = make_tick("TSLA", "1", "10", Side::Bid, 0);
246 let wrong_side = make_tick("AAPL", "1", "10", Side::Ask, 0);
247 let wrong_qty = make_tick("AAPL", "1", "1", Side::Bid, 0);
248 assert!(f.matches(&ok));
249 assert!(!f.matches(&wrong_sym));
250 assert!(!f.matches(&wrong_side));
251 assert!(!f.matches(&wrong_qty));
252 }
253
254 #[test]
255 fn test_tick_replayer_sorts_by_timestamp() {
256 let ticks = vec![
257 make_tick("A", "1", "1", Side::Bid, 300),
258 make_tick("A", "1", "1", Side::Bid, 100),
259 make_tick("A", "1", "1", Side::Bid, 200),
260 ];
261 let mut replayer = TickReplayer::new(ticks);
262 let t1 = replayer.next_tick().unwrap();
263 assert_eq!(t1.timestamp.0, 100);
264 let t2 = replayer.next_tick().unwrap();
265 assert_eq!(t2.timestamp.0, 200);
266 let t3 = replayer.next_tick().unwrap();
267 assert_eq!(t3.timestamp.0, 300);
268 }
269
270 #[test]
271 fn test_tick_replayer_next_tick_sequential() {
272 let ticks = vec![
273 make_tick("A", "1", "1", Side::Bid, 1),
274 make_tick("A", "1", "1", Side::Bid, 2),
275 ];
276 let mut replayer = TickReplayer::new(ticks);
277 assert!(replayer.next_tick().is_some());
278 assert!(replayer.next_tick().is_some());
279 assert!(replayer.next_tick().is_none());
280 }
281
282 #[test]
283 fn test_tick_replayer_reset_restarts() {
284 let ticks = vec![make_tick("A", "1", "1", Side::Bid, 1)];
285 let mut replayer = TickReplayer::new(ticks);
286 let _ = replayer.next_tick();
287 assert!(replayer.next_tick().is_none());
288 replayer.reset();
289 assert!(replayer.next_tick().is_some());
290 }
291
292 #[test]
293 fn test_tick_replayer_remaining() {
294 let ticks = vec![
295 make_tick("A", "1", "1", Side::Bid, 1),
296 make_tick("A", "1", "1", Side::Bid, 2),
297 make_tick("A", "1", "1", Side::Bid, 3),
298 ];
299 let mut replayer = TickReplayer::new(ticks);
300 assert_eq!(replayer.remaining(), 3);
301 let _ = replayer.next_tick();
302 assert_eq!(replayer.remaining(), 2);
303 }
304}