1use crate::model::{OrderSide, Tick, next_revision};
11use chrono::{DateTime, Utc};
12use std::collections::{HashMap, VecDeque};
13
14#[derive(Clone, Debug, PartialEq)]
16#[non_exhaustive]
17pub struct ConsumedOrder {
18 pub resting_order_id: Option<u64>,
21 pub price: f64,
23 pub size: f64,
25}
26
27#[derive(Clone, Debug, PartialEq)]
30#[non_exhaustive]
31pub struct CombinedTrade {
32 pub aggressor_order_id: Option<u64>,
34 pub first_time: DateTime<Utc>,
36 pub last_time: DateTime<Utc>,
38 pub first_price: f64,
40 pub last_price: f64,
42 pub price: f64,
44 pub size: f64,
46 pub fills: usize,
48 pub side: Option<OrderSide>,
50 pub consumed: Vec<ConsumedOrder>,
54 pub complete: bool,
57}
58
59pub fn combine_trades<'a>(trades: impl IntoIterator<Item = &'a Tick>) -> Vec<CombinedTrade> {
63 let mut indices: HashMap<u64, usize> = HashMap::new();
64 let mut combined: Vec<CombinedTrade> = Vec::new();
65 for fill in trades.into_iter().filter(|t| valid(t)) {
66 if let Some(&at) = fill
67 .aggressor_order_id
68 .as_ref()
69 .and_then(|id| indices.get(id))
70 {
71 let trade = &mut combined[at];
72 let scale = trade.size.max(fill.size);
73 if scale > 0.0 {
74 let weight = (fill.size / scale) / (trade.size / scale + fill.size / scale);
75 trade.price += (fill.price - trade.price) * weight;
76 }
77 trade.size = (trade.size + fill.size).min(f64::MAX);
78 trade.last_time = fill.ts;
79 trade.last_price = fill.price;
80 trade.fills += 1;
81 if trade.side != fill.side {
82 trade.side = None;
83 }
84 trade.consume(fill);
85 } else {
86 if let Some(id) = fill.aggressor_order_id {
87 indices.insert(id, combined.len());
88 }
89 let mut trade = CombinedTrade {
90 aggressor_order_id: fill.aggressor_order_id,
91 first_time: fill.ts,
92 last_time: fill.ts,
93 first_price: fill.price,
94 last_price: fill.price,
95 price: fill.price,
96 size: fill.size,
97 fills: 1,
98 side: fill.side,
99 consumed: Vec::new(),
100 complete: fill.aggressor_order_id.is_some(),
101 };
102 trade.consume(fill);
103 combined.push(trade);
104 }
105 }
106 combined
107}
108
109impl CombinedTrade {
110 fn consume(&mut self, fill: &Tick) {
112 self.complete &= fill.resting_order_id.is_some();
113 match self.consumed.last_mut().filter(|c| {
114 fill.resting_order_id.is_some()
115 && c.resting_order_id == fill.resting_order_id
116 && c.price == fill.price
117 }) {
118 Some(order) => order.size = (order.size + fill.size).min(f64::MAX),
119 None => self.consumed.push(ConsumedOrder {
120 resting_order_id: fill.resting_order_id,
121 price: fill.price,
122 size: fill.size,
123 }),
124 }
125 }
126}
127
128pub(crate) struct Trades {
130 trades: VecDeque<Tick>,
131 capacity: usize,
132 ids: HashMap<u64, usize>,
134 front: usize,
136 paged: usize,
138 paged_until: Option<DateTime<Utc>>,
140 paged_without_id_until: Option<DateTime<Utc>>,
143 lost_until: Option<DateTime<Utc>>,
145 revision: u64,
147}
148
149impl Trades {
150 pub(crate) fn new(capacity: usize) -> Self {
151 Self {
152 trades: VecDeque::new(),
153 capacity,
154 ids: HashMap::new(),
155 front: 0,
156 paged: 0,
157 paged_until: None,
158 paged_without_id_until: None,
159 lost_until: None,
160 revision: next_revision(),
161 }
162 }
163
164 #[cfg_attr(
166 not(feature = "widget"),
167 expect(dead_code, reason = "only the chart widget consumes a feed")
168 )]
169 pub(crate) fn revision(&self) -> u64 {
170 self.revision
171 }
172
173 #[cfg_attr(
175 not(feature = "widget"),
176 expect(dead_code, reason = "only the chart widget consumes a feed")
177 )]
178 pub(crate) fn lost_until(&self) -> Option<DateTime<Utc>> {
179 self.lost_until
180 }
181
182 #[cfg_attr(
184 not(any(feature = "widget", test)),
185 expect(dead_code, reason = "only the chart widget consumes a feed")
186 )]
187 pub(crate) fn live(&self) -> impl Iterator<Item = &Tick> {
188 self.trades.iter().skip(self.paged)
189 }
190
191 pub(crate) fn empty(&self) -> Self {
193 Self::new(self.capacity)
194 }
195
196 pub(crate) fn iter(&self) -> impl Iterator<Item = &Tick> {
197 self.trades.iter()
198 }
199
200 #[cfg_attr(
202 not(feature = "widget"),
203 expect(dead_code, reason = "only the chart widget consumes a feed")
204 )]
205 pub(crate) fn oldest(&self) -> Option<DateTime<Utc>> {
206 self.trades.iter().map(|trade| trade.ts).min()
207 }
208
209 #[cfg_attr(
211 not(feature = "widget"),
212 expect(dead_code, reason = "only the chart widget consumes a feed")
213 )]
214 pub(crate) fn room(&self) -> usize {
215 self.capacity.saturating_sub(self.trades.len())
216 }
217
218 pub(crate) fn push(&mut self, trade: Tick) {
221 self.insert(trade, &mut |_, _| ());
222 }
223
224 fn insert(&mut self, trade: Tick, changed: &mut impl FnMut(&Tick, bool)) {
227 if trade.id.is_some_and(|id| self.ids.contains_key(&id)) {
228 self.annotate(&trade);
229 return;
230 }
231 if let Some(id) = trade.id {
232 self.ids
233 .insert(id, self.front.wrapping_add(self.trades.len()));
234 }
235 self.revision = next_revision();
236 changed(&trade, true);
237 self.trades.push_back(trade);
238 if self.trades.len() > self.capacity
239 && let Some(evicted) = self.trades.pop_front()
240 {
241 self.front = self.front.wrapping_add(1);
242 self.paged = self.paged.saturating_sub(1);
243 if let Some(id) = evicted.id {
244 self.ids.remove(&id);
245 }
246 self.lost_until = self.lost_until.max(Some(evicted.ts));
247 changed(&evicted, false);
248 }
249 }
250
251 fn annotate(&mut self, trade: &Tick) {
253 if (trade.aggressor_order_id.is_some() || trade.resting_order_id.is_some())
254 && let Some(at) = trade.id.and_then(|id| self.ids.get(&id))
255 {
256 let held = &mut self.trades[at.wrapping_sub(self.front)];
257 held.aggressor_order_id = held.aggressor_order_id.or(trade.aggressor_order_id);
258 held.resting_order_id = held.resting_order_id.or(trade.resting_order_id);
259 }
260 }
261
262 pub(crate) fn append(&mut self, newer: Trades, changed: &mut impl FnMut(&Tick, bool)) {
265 self.lost_until = self.lost_until.max(newer.lost_until);
266 for trade in newer.trades {
267 let paged_until = match trade.id {
268 Some(_) => self.paged_without_id_until,
269 None => self.paged_until,
270 };
271 if trade.id.is_some_and(|id| self.ids.contains_key(&id))
272 || paged_until.is_none_or(|until| trade.ts > until)
273 {
274 self.insert(trade, changed);
275 }
276 }
277 }
278
279 pub(crate) fn prepend(
283 &mut self,
284 mut page: Vec<Tick>,
285 changed: &mut impl FnMut(&Tick, bool),
286 ) -> u64 {
287 let received = page.len();
288 page.retain(valid);
289 let rejected = (received - page.len()) as u64;
290 for trade in &page {
291 self.annotate(trade);
292 }
293 page.sort_by_key(|trade| trade.ts);
295 if let Some(oldest) = self.trades.iter().map(|trade| trade.ts).min() {
296 let ids_at_oldest = (self.trades.iter())
297 .filter(|held| held.ts == oldest)
298 .all(|held| held.id.is_some());
299 page.retain(|trade| {
300 trade.ts < oldest || (trade.ts == oldest && trade.id.is_some() && ids_at_oldest)
301 });
302 }
303 let mut room = self.capacity.saturating_sub(self.trades.len());
304 let mut added = Vec::new();
305 for trade in page.into_iter().rev() {
306 if trade.id.is_some_and(|id| self.ids.contains_key(&id)) {
307 continue;
308 }
309 if room == 0 {
310 self.lost_until = self.lost_until.max(Some(trade.ts));
312 break;
313 }
314 room -= 1;
315 if let Some(id) = trade.id {
316 self.ids
317 .insert(id, self.front.wrapping_sub(added.len() + 1));
318 }
319 added.push(trade);
320 }
321 for trade in added {
322 self.paged_until = self.paged_until.max(Some(trade.ts));
323 if trade.id.is_none() {
324 self.paged_without_id_until = self.paged_without_id_until.max(Some(trade.ts));
325 }
326 self.revision = next_revision();
327 changed(&trade, true);
328 self.front = self.front.wrapping_sub(1);
329 self.paged += 1;
330 self.trades.push_front(trade);
331 }
332 rejected
333 }
334}
335
336pub(super) fn valid(trade: &Tick) -> bool {
338 trade.price.is_finite() && trade.size.is_finite() && trade.size >= 0.0
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use chrono::TimeDelta;
345
346 fn trade(second: i64, price: f64) -> Tick {
348 let ts = "2024-03-15T10:00:00Z".parse::<DateTime<Utc>>().unwrap();
349 Tick::new(ts + TimeDelta::seconds(second), price, 1.0, None)
350 }
351
352 enum Step {
353 Live(Vec<Tick>),
355 Page(Vec<Tick>),
356 }
357 use Step::{Live, Page};
358
359 #[test]
361 fn combines_only_fills_of_the_same_aggressor() {
362 let fill = |second, price, size, order| {
363 let mut t = trade(second, price);
364 t.size = size;
365 t.aggressor_order_id = order;
366 t
367 };
368 #[rustfmt::skip]
369 let cases = [
370 ("empty input", 10, vec![], vec![]),
371 ("zero total size keeps the first price", 10,
372 vec![Live(vec![fill(1, 100.0, 0.0, Some(7)), fill(2, 101.0, 0.0, Some(7))])],
373 vec![(0.0, 100.0, 101.0, 100.0, 2)]),
374 ("invalid fills do not combine", 10,
375 vec![Live(vec![fill(1, f64::NAN, 1.0, Some(7)), fill(2, 100.0, f64::NAN, Some(7)),
376 fill(3, 100.0, -1.0, Some(7)), fill(4, 100.0, 1.0, Some(7))])],
377 vec![(1.0, 100.0, 100.0, 100.0, 1)]),
378 ("three prices", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
379 fill(2, 101.0, 3.0, Some(7)), fill(3, 102.0, 5.0, Some(7))])],
380 vec![(10.0, 100.0, 102.0, 101.3, 3)]),
381 ("interleaved ids", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
382 fill(2, 90.0, 1.0, Some(8)), fill(3, 102.0, 2.0, Some(7))])],
383 vec![(4.0, 100.0, 102.0, 101.0, 2), (1.0, 90.0, 90.0, 90.0, 1)]),
384 ("unknown ids stay separate", 10, vec![Live(vec![fill(1, 100.0, 2.0, None),
385 fill(2, 100.0, 3.0, None)])],
386 vec![(2.0, 100.0, 100.0, 100.0, 1), (3.0, 100.0, 100.0, 100.0, 1)]),
387 ("history and live fills", 10, vec![Page(vec![fill(1, 100.0, 2.0, Some(7))]),
388 Live(vec![fill(2, 102.0, 2.0, Some(7))])],
389 vec![(4.0, 100.0, 102.0, 101.0, 2)]),
390 ("separate drains", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7))]),
391 Live(vec![fill(2, 102.0, 2.0, Some(7))])],
392 vec![(4.0, 100.0, 102.0, 101.0, 2)]),
393 ("a page completes order ids on retained fills", 10,
394 vec![Live(vec![fill(1, 100.0, 2.0, None).with_id(1), fill(2, 102.0, 2.0, None).with_id(2)]),
395 Page(vec![fill(1, 100.0, 2.0, Some(7)).with_id(1), fill(2, 102.0, 2.0, Some(7)).with_id(2)])],
396 vec![(4.0, 100.0, 102.0, 101.0, 2)]),
397 ("only retained fills count", 2, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
398 fill(2, 101.0, 3.0, Some(7))]), Live(vec![fill(3, 102.0, 1.0, Some(7))])],
399 vec![(4.0, 101.0, 102.0, 101.25, 2)]),
400 ];
401 for (name, capacity, steps, expected) in cases {
402 let mut store = Trades::new(capacity);
403 for step in steps {
404 match step {
405 Live(trades) => {
406 let mut pending = store.empty();
407 trades.into_iter().for_each(|t| pending.push(t));
408 store.append(pending, &mut |_, _| ());
409 }
410 Page(page) => {
411 store.prepend(page, &mut |_, _| ());
412 }
413 }
414 }
415 let combined = combine_trades(store.iter());
416 let held: Vec<_> = combined
417 .iter()
418 .map(|t| (t.size, t.first_price, t.last_price, t.price, t.fills))
419 .collect();
420 assert_eq!(held.len(), expected.len(), "{name}");
421 for (actual, expected) in held.iter().zip(expected) {
422 assert_eq!(
423 (actual.0, actual.1, actual.2, actual.4),
424 (expected.0, expected.1, expected.2, expected.4),
425 "{name}"
426 );
427 assert!((actual.3 - expected.3).abs() < 1e-10, "{name}: VWAP");
428 }
429 }
430 let fills = [
432 fill(1, 99.99, 1.0, Some(7)),
433 fill(2, 99.99, 2.0, Some(7)),
434 fill(3, 99.99, 3.0, Some(7)),
435 ];
436 assert_eq!(combine_trades(&fills)[0].price, 99.99);
437 }
438
439 #[test]
443 fn the_breakdown_sums_to_the_retained_fills_and_reports_missing_evidence() {
444 let fill = |second, price, size, resting| {
445 let mut t = trade(second, price).with_aggressor_order_id(7);
446 t.size = size;
447 t.resting_order_id = resting;
448 t
449 };
450 let fills = || {
451 vec![
452 fill(1, 100.0, 2.0, Some(1)),
453 fill(2, 100.0, 1.0, Some(1)),
454 fill(3, 101.0, 4.0, Some(2)),
455 fill(4, 101.0, 3.0, None),
456 ]
457 };
458 for (capacity, want, complete) in [
459 (
460 10,
461 vec![
462 (Some(1), 100.0, 3.0),
463 (Some(2), 101.0, 4.0),
464 (None, 101.0, 3.0),
465 ],
466 false,
467 ),
468 (2, vec![(Some(2), 101.0, 4.0), (None, 101.0, 3.0)], false),
470 ] {
471 let mut store = Trades::new(capacity);
472 fills().into_iter().for_each(|t| store.push(t));
473 let combined = combine_trades(store.iter());
474 let trade = &combined[0];
475 let got: Vec<_> = trade
476 .consumed
477 .iter()
478 .map(|c| (c.resting_order_id, c.price, c.size))
479 .collect();
480 assert_eq!(
481 (got, trade.complete),
482 (want, complete),
483 "capacity {capacity}"
484 );
485 assert_eq!(
486 trade.consumed.iter().map(|c| c.size).sum::<f64>(),
487 trade.size
488 );
489 }
490 let named: Vec<_> = fills().into_iter().take(3).collect();
491 assert!(combine_trades(&named)[0].complete);
492 }
493
494 #[test]
500 fn the_store_is_bounded_and_joins_pages_without_repeats() {
501 let id = |trade: Tick, id| trade.with_id(id);
502 let later = |mut trade: Tick, micros| {
503 trade.ts += TimeDelta::microseconds(micros);
504 trade
505 };
506 #[rustfmt::skip]
507 let cases = [
508 ("zero capacity holds no ids", 0,
509 vec![Live(vec![id(trade(1, 1.0), 1)]), Page(vec![id(trade(0, 0.0), 0)]),
510 Live(vec![id(trade(2, 2.0), 1)])], vec![], 0, vec![]),
511 ("the oldest trades make room", 3,
512 vec![Live(vec![id(trade(10, 10.0), 10), trade(11, 11.0), trade(12, 12.0)]),
513 Live(vec![trade(13, 13.0)])],
514 vec![11.0, 12.0, 13.0], 0, vec![11.0, 12.0, 13.0]),
515 ("a page fills free room in front, without invalid or repeated trades", 4,
516 vec![Live(vec![trade(10, 10.0), trade(11, 11.0), trade(12, 12.0)]),
517 Page(vec![trade(9, 9.0), trade(8, f64::NAN), trade(11, 99.0), trade(7, 7.0)])],
518 vec![9.0, 10.0, 11.0, 12.0], 1, vec![10.0, 11.0, 12.0]),
519 ("a full store takes nothing from a page", 2,
520 vec![Live(vec![trade(10, 10.0), trade(11, 11.0)]), Page(vec![trade(1, 1.0)])],
521 vec![10.0, 11.0], 0, vec![10.0, 11.0]),
522 ("with ids, a page keeps a trade at the instant of the oldest live trade", 10,
523 vec![Live(vec![id(trade(5, 3.0), 3), id(trade(5, 4.0), 4)]),
524 Page(vec![id(trade(4, 1.0), 1), id(trade(5, 2.0), 2), id(trade(5, 3.0), 3),
525 id(trade(5, 4.0), 4)])],
526 vec![1.0, 2.0, 3.0, 4.0], 0, vec![3.0, 4.0]),
527 ("without ids, a page drops trades at the instant of the oldest live trade", 10,
528 vec![Live(vec![trade(5, 3.0), trade(5, 4.0)]),
529 Page(vec![trade(4, 1.0), trade(5, 2.0), trade(5, 3.0), trade(5, 4.0)])],
530 vec![1.0, 3.0, 4.0], 0, vec![3.0, 4.0]),
531 ("with ids, live trades a page already added are dropped", 10,
532 vec![Page(vec![id(trade(1, 1.0), 1), id(trade(2, 2.0), 2)]),
533 Live(vec![id(trade(2, 2.0), 2), id(trade(2, 2.5), 5), id(trade(3, 3.0), 3)])],
534 vec![1.0, 2.0, 2.5, 3.0], 0, vec![2.5, 3.0]),
535 ("without ids, live trades at or before the newest page trade are dropped", 10,
536 vec![Page(vec![trade(1, 1.0), trade(2, 2.0)]),
537 Live(vec![trade(2, 2.0), trade(3, 3.0)])],
538 vec![1.0, 2.0, 3.0], 0, vec![3.0]),
539 ("with ids, page trades after the oldest live trade are left to live", 10,
540 vec![Live(vec![id(trade(5, 3.0), 3)]),
541 Page(vec![id(trade(4, 1.0), 1), id(trade(5, 3.0), 3), id(trade(6, 4.0), 4),
542 id(trade(7, 5.0), 5)]),
543 Live(vec![id(trade(6, 4.0), 4), id(trade(7, 5.0), 5)])],
544 vec![1.0, 3.0, 4.0, 5.0], 0, vec![3.0, 4.0, 5.0]),
545 ("with ids, a replayed live trade is dropped wherever the held one sits", 10,
546 vec![Page(vec![id(trade(8, 8.0), 8), id(trade(9, 9.0), 9)]),
547 Live(vec![id(trade(7, 7.0), 7), id(trade(8, 8.0), 8), id(trade(9, 9.0), 9)])],
548 vec![8.0, 9.0, 7.0], 0, vec![7.0]),
549 ("with ids, a live trade stamped later than the page's copy is dropped", 10,
550 vec![Page(vec![id(trade(5, 5.0), 5)]),
551 Live(vec![id(later(trade(5, 5.0), 400), 5)])],
552 vec![5.0], 0, vec![]),
553 ("with ids, a page trade stamped earlier than the live copy is dropped", 10,
554 vec![Live(vec![id(later(trade(5, 5.0), 400), 5)]),
555 Page(vec![id(trade(4, 4.0), 4), id(trade(5, 5.0), 5)])],
556 vec![4.0, 5.0], 0, vec![5.0]),
557 ("a page without ids drops live trades with ids at or before its newest trade", 10,
558 vec![Page(vec![trade(1, 1.0), trade(2, 2.0)]),
559 Live(vec![id(trade(2, 2.0), 2), id(trade(3, 3.0), 3)])],
560 vec![1.0, 2.0, 3.0], 0, vec![3.0]),
561 ("a page with ids drops trades at the instant of a held trade without one", 10,
562 vec![Live(vec![trade(5, 5.0)]),
563 Page(vec![id(trade(4, 4.0), 4), id(trade(5, 5.0), 5)])],
564 vec![4.0, 5.0], 0, vec![5.0]),
565 ("a page is sorted by time, keeping the order of trades sharing a time", 10,
566 vec![Page(vec![trade(5, 4.0), trade(5, 3.0), trade(4, 2.0)])],
567 vec![2.0, 4.0, 3.0], 0, vec![]),
568 ("eviction removes history before the live suffix", 4,
569 vec![Live(vec![trade(10, 10.0), trade(11, 11.0)]),
570 Page(vec![trade(8, 8.0), trade(9, 9.0)]), Live(vec![trade(12, 12.0)])],
571 vec![9.0, 10.0, 11.0, 12.0], 0, vec![10.0, 11.0, 12.0]),
572 ];
573 for (name, capacity, steps, prices, rejected, live_prices) in cases {
574 let mut store = Trades::new(capacity);
575 let mut dropped = 0;
576 for step in steps {
577 match step {
578 Live(trades) => {
579 let mut pending = store.empty();
580 trades.into_iter().for_each(|trade| pending.push(trade));
581 store.append(pending, &mut |_, _| ());
582 }
583 Page(page) => dropped += store.prepend(page, &mut |_, _| ()),
584 }
585 }
586 let held = Vec::from_iter(store.iter().map(|trade| trade.price));
587 let live = Vec::from_iter(store.live().map(|trade| trade.price));
588 assert_eq!(
589 (held, dropped, live),
590 (prices, rejected, live_prices),
591 "{name}"
592 );
593 let ids = store.iter().filter(|trade| trade.id.is_some()).count();
594 assert_eq!(store.ids.len(), ids, "{name}: held ids");
595 let repeats: Vec<_> = store.iter().filter(|t| t.id.is_some()).cloned().collect();
596 for trade in repeats {
597 let id = trade.id.unwrap();
598 store.push(trade.with_aggressor_order_id(id).with_resting_order_id(id));
599 }
600 for trade in store.iter().filter(|t| t.id.is_some()) {
601 assert_eq!(trade.aggressor_order_id, trade.id, "{name}");
602 assert_eq!(trade.resting_order_id, trade.id, "{name}");
603 }
604 }
605 }
606
607 #[test]
609 fn replay_with_order_ids_annotates_retained_fills() {
610 let count = 100_000;
611 let fill = |id: usize| trade(id as i64, 100.0).with_id(id as u64);
612 for path in ["push", "append", "prepend"] {
613 let mut store = Trades::new(count);
614 for id in 0..count {
615 store.push(fill(id));
616 }
617 let page: Vec<_> = (0..count)
618 .map(|id| fill(id).with_aggressor_order_id(id as u64 + 1))
619 .collect();
620 match path {
621 "push" => page.into_iter().for_each(|t| store.push(t)),
622 "append" => {
623 let mut pending = store.empty();
624 page.into_iter().for_each(|t| pending.push(t));
625 store.append(pending, &mut |_, _| panic!("repeated fill"));
626 }
627 _ => {
628 store.prepend(page, &mut |_, _| panic!("repeated fill"));
629 }
630 }
631 assert_eq!(store.iter().count(), count, "{path}");
632 for (id, held) in store.iter().enumerate() {
633 assert_eq!(held.aggressor_order_id, Some(id as u64 + 1), "{path}");
634 }
635 }
636 }
637}