1use navi_types::BarStartEvent;
8
9use crate::{BarState, Event};
10
11#[derive(Debug, Clone, Default)]
13pub struct EventCompactionRules {
14 pub filter_log: bool,
16 pub filter_alert: bool,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum RealtimeConfirmReason {
23 Rollover,
26 StreamEnd,
29}
30
31#[derive(Debug, Clone)]
33pub struct RealtimeConfirmedInfo {
34 pub bar_index: usize,
36 pub bar_time: i64,
38 pub reason: RealtimeConfirmReason,
40 pub event_count: usize,
56}
57
58pub trait EventCompactionSink {
60 fn on_event(&mut self, event: Event);
62 fn on_realtime_confirmed(&mut self, info: RealtimeConfirmedInfo);
64}
65
66struct RealtimeBarTracking {
68 bar_index: usize,
69 bar_time: i64,
70 event_count: usize,
71}
72
73pub struct EventCompactor {
83 rules: EventCompactionRules,
84 current_realtime: Option<RealtimeBarTracking>,
85}
86
87impl EventCompactor {
88 pub fn new(rules: EventCompactionRules) -> Self {
90 Self {
91 rules,
92 current_realtime: None,
93 }
94 }
95
96 pub fn push<S>(&mut self, event: Event, sink: &mut S)
103 where
104 S: EventCompactionSink,
105 {
106 match &event {
107 Event::BarStart(bs) => self.handle_bar_start(bs.clone(), event, sink),
108
109 Event::BarEnd => {
110 if let Some(rt) = &mut self.current_realtime {
111 rt.event_count += 1;
112 }
113 sink.on_event(event);
114 }
115
116 Event::SessionInfo { .. } | Event::HistoryEnd | Event::Warning(_) => {
118 sink.on_event(event);
119 }
120
121 Event::Log(_) => {
122 if !self.rules.filter_log {
123 if let Some(rt) = &mut self.current_realtime {
124 rt.event_count += 1;
125 }
126 sink.on_event(event);
127 }
128 }
129
130 Event::Alert(_) => {
131 if !self.rules.filter_alert {
132 if let Some(rt) = &mut self.current_realtime {
133 rt.event_count += 1;
134 }
135 sink.on_event(event);
136 }
137 }
138
139 Event::Draw(_) | Event::Strategy(_) => {
140 if let Some(rt) = &mut self.current_realtime {
141 rt.event_count += 1;
142 }
143 sink.on_event(event);
144 }
145 }
146 }
147
148 pub fn finish<S>(&mut self, sink: &mut S)
153 where
154 S: EventCompactionSink,
155 {
156 self.emit_confirmed(RealtimeConfirmReason::StreamEnd, sink);
157 }
158
159 fn handle_bar_start<S>(&mut self, bs: BarStartEvent, event: Event, sink: &mut S)
160 where
161 S: EventCompactionSink,
162 {
163 match bs.bar_state {
164 BarState::History => {
165 sink.on_event(event);
167 }
168
169 BarState::RealtimeNew => {
170 let rollover = self
173 .current_realtime
174 .as_ref()
175 .is_some_and(|rt| rt.bar_time != bs.candlestick.time);
176 if rollover {
177 self.emit_confirmed(RealtimeConfirmReason::Rollover, sink);
178 }
179 self.current_realtime = Some(RealtimeBarTracking {
181 bar_index: bs.bar_index,
182 bar_time: bs.candlestick.time,
183 event_count: 1, });
185 sink.on_event(event);
186 }
187
188 BarState::RealtimeUpdate | BarState::RealtimeConfirmed => {
189 if let Some(rt) = &mut self.current_realtime {
190 rt.event_count += 1;
191 }
192 sink.on_event(event);
193 }
194 }
195 }
196
197 fn emit_confirmed<S>(&mut self, reason: RealtimeConfirmReason, sink: &mut S)
198 where
199 S: EventCompactionSink,
200 {
201 if let Some(rt) = self.current_realtime.take() {
202 sink.on_realtime_confirmed(RealtimeConfirmedInfo {
203 bar_index: rt.bar_index,
204 bar_time: rt.bar_time,
205 reason,
206 event_count: rt.event_count,
207 });
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use navi_types::{BarStartEvent, BarState, Candlestick};
215 use navi_visuals::DrawEvent;
216
217 use super::*;
218 use crate::events::{AlertEvent, LogEvent, LogLevel};
219
220 fn make_candlestick(time: i64) -> Candlestick {
223 use crate::TradeSession;
224 Candlestick::new(time, 1.0, 2.0, 0.5, 1.5, 1000.0, 0.0, TradeSession::Regular)
225 }
226
227 fn bar_start(bar_index: usize, time: i64, state: BarState) -> Event {
228 Event::BarStart(BarStartEvent {
229 bar_index,
230 candlestick: make_candlestick(time),
231 bar_state: state,
232 })
233 }
234
235 #[derive(Default)]
237 struct CollectSink {
238 events: Vec<Event>,
239 confirmations: Vec<RealtimeConfirmedInfo>,
240 }
241
242 impl EventCompactionSink for CollectSink {
243 fn on_event(&mut self, event: Event) {
244 self.events.push(event);
245 }
246 fn on_realtime_confirmed(&mut self, info: RealtimeConfirmedInfo) {
247 self.confirmations.push(info);
248 }
249 }
250
251 const T0: i64 = 1_700_000_000_000;
252 const DAY: i64 = 86_400_000;
253
254 #[test]
257 fn history_bars_pass_through() {
258 let mut compactor = EventCompactor::new(EventCompactionRules::default());
259 let mut sink = CollectSink::default();
260
261 compactor.push(bar_start(0, T0, BarState::History), &mut sink);
262 compactor.push(Event::BarEnd, &mut sink);
263 compactor.push(bar_start(1, T0 + DAY, BarState::History), &mut sink);
264 compactor.push(Event::BarEnd, &mut sink);
265 compactor.finish(&mut sink);
266
267 assert_eq!(sink.events.len(), 4);
268 assert!(
269 sink.confirmations.is_empty(),
270 "no realtime bars → no confirmations"
271 );
272 }
273
274 #[test]
277 fn realtime_rollover() {
278 let mut compactor = EventCompactor::new(EventCompactionRules::default());
279 let mut sink = CollectSink::default();
280
281 compactor.push(bar_start(1, T0 + DAY, BarState::RealtimeNew), &mut sink);
283 compactor.push(Event::BarEnd, &mut sink);
284 compactor.push(bar_start(1, T0 + DAY, BarState::RealtimeUpdate), &mut sink);
285 compactor.push(Event::BarEnd, &mut sink);
286 compactor.push(
287 bar_start(1, T0 + DAY, BarState::RealtimeConfirmed),
288 &mut sink,
289 );
290 compactor.push(Event::BarEnd, &mut sink);
291
292 assert!(sink.confirmations.is_empty());
294
295 compactor.push(bar_start(2, T0 + DAY * 2, BarState::RealtimeNew), &mut sink);
297 compactor.push(Event::BarEnd, &mut sink);
298
299 assert_eq!(sink.confirmations.len(), 1);
301 let c = &sink.confirmations[0];
302 assert_eq!(c.bar_index, 1);
303 assert_eq!(c.bar_time, T0 + DAY);
304 assert_eq!(c.reason, RealtimeConfirmReason::Rollover);
305 assert_eq!(c.event_count, 6);
307
308 compactor.finish(&mut sink);
310 assert_eq!(sink.confirmations.len(), 2);
311 assert_eq!(
312 sink.confirmations[1].reason,
313 RealtimeConfirmReason::StreamEnd
314 );
315 }
316
317 #[test]
320 fn realtime_stream_end() {
321 let mut compactor = EventCompactor::new(EventCompactionRules::default());
322 let mut sink = CollectSink::default();
323
324 compactor.push(bar_start(0, T0, BarState::RealtimeNew), &mut sink);
325 compactor.push(Event::BarEnd, &mut sink);
326 compactor.finish(&mut sink);
327
328 assert_eq!(sink.confirmations.len(), 1);
329 assert_eq!(
330 sink.confirmations[0].reason,
331 RealtimeConfirmReason::StreamEnd
332 );
333 assert_eq!(sink.confirmations[0].event_count, 2); }
335
336 #[test]
339 fn filter_log_drops_event() {
340 let rules = EventCompactionRules {
341 filter_log: true,
342 ..Default::default()
343 };
344 let mut compactor = EventCompactor::new(rules);
345 let mut sink = CollectSink::default();
346
347 compactor.push(bar_start(0, T0, BarState::RealtimeNew), &mut sink);
348 compactor.push(
349 Event::Log(LogEvent {
350 level: LogLevel::Info,
351 message: "hello".into(),
352 }),
353 &mut sink,
354 );
355 compactor.push(Event::BarEnd, &mut sink);
356 compactor.finish(&mut sink);
357
358 let log_count = sink
360 .events
361 .iter()
362 .filter(|e| matches!(e, Event::Log(_)))
363 .count();
364 assert_eq!(log_count, 0);
365
366 assert_eq!(sink.confirmations[0].event_count, 2); }
369
370 #[test]
373 fn bar_outside_events_not_counted() {
374 let mut compactor = EventCompactor::new(EventCompactionRules::default());
375 let mut sink = CollectSink::default();
376
377 compactor.push(Event::HistoryEnd, &mut sink);
379
380 compactor.push(bar_start(0, T0, BarState::RealtimeNew), &mut sink);
382 compactor.push(Event::BarEnd, &mut sink);
383 compactor.finish(&mut sink);
384
385 let forwarded_outside: Vec<_> = sink
387 .events
388 .iter()
389 .filter(|e| matches!(e, Event::HistoryEnd))
390 .collect();
391 assert_eq!(forwarded_outside.len(), 1);
392
393 assert_eq!(sink.confirmations[0].event_count, 2);
395 }
396
397 #[test]
400 fn filter_alert_drops_event() {
401 let rules = EventCompactionRules {
402 filter_alert: true,
403 ..Default::default()
404 };
405 let mut compactor = EventCompactor::new(rules);
406 let mut sink = CollectSink::default();
407
408 compactor.push(bar_start(0, T0, BarState::RealtimeNew), &mut sink);
409 compactor.push(
410 Event::Alert(AlertEvent {
411 id: None,
412 message: "alert!".into(),
413 }),
414 &mut sink,
415 );
416 compactor.push(Event::BarEnd, &mut sink);
417 compactor.finish(&mut sink);
418
419 let alert_count = sink
420 .events
421 .iter()
422 .filter(|e| matches!(e, Event::Alert(_)))
423 .count();
424 assert_eq!(alert_count, 0);
425 assert_eq!(sink.confirmations[0].event_count, 2);
426 }
427
428 #[test]
431 fn draw_events_counted_in_realtime() {
432 let mut compactor = EventCompactor::new(EventCompactionRules::default());
433 let mut sink = CollectSink::default();
434
435 compactor.push(bar_start(0, T0, BarState::RealtimeNew), &mut sink);
436 compactor.push(Event::Draw(DrawEvent::NewBar { bar_index: 0 }), &mut sink);
437 compactor.push(Event::BarEnd, &mut sink);
438 compactor.finish(&mut sink);
439
440 assert_eq!(sink.confirmations[0].event_count, 3);
442 }
443
444 #[test]
447 fn history_does_not_start_realtime_tracking() {
448 let mut compactor = EventCompactor::new(EventCompactionRules::default());
449 let mut sink = CollectSink::default();
450
451 compactor.push(bar_start(0, T0, BarState::History), &mut sink);
452 compactor.push(Event::BarEnd, &mut sink);
453 compactor.finish(&mut sink);
454
455 assert!(sink.confirmations.is_empty());
456 assert!(compactor.current_realtime.is_none());
457 }
458
459 #[test]
462 fn equivalence_chart_accumulator() {
463 use navi_visuals::{ChartAccumulator, ChartEvent};
464
465 let events_with_log = vec![
466 Event::HistoryEnd,
467 bar_start(0, T0, BarState::RealtimeNew),
468 Event::Draw(DrawEvent::NewBar { bar_index: 0 }),
469 Event::Log(LogEvent {
470 level: LogLevel::Info,
471 message: "noise".into(),
472 }),
473 Event::BarEnd,
474 ];
475 let events_without_log: Vec<Event> = events_with_log
476 .iter()
477 .filter(|e| !matches!(e, Event::Log(_)))
478 .cloned()
479 .collect();
480
481 let mut ref_acc = ChartAccumulator::new();
483 for event in events_without_log {
484 if let Ok(ce) = ChartEvent::try_from(event) {
485 ref_acc.apply(ce);
486 }
487 }
488
489 struct ChartSink {
491 acc: ChartAccumulator,
492 }
493 impl EventCompactionSink for ChartSink {
494 fn on_event(&mut self, event: Event) {
495 if let Ok(ce) = ChartEvent::try_from(event) {
496 self.acc.apply(ce);
497 }
498 }
499 fn on_realtime_confirmed(&mut self, _: RealtimeConfirmedInfo) {}
500 }
501 let mut sink = ChartSink {
502 acc: ChartAccumulator::new(),
503 };
504 let mut compactor = EventCompactor::new(EventCompactionRules {
505 filter_log: true,
506 ..Default::default()
507 });
508 for event in events_with_log {
509 compactor.push(event, &mut sink);
510 }
511 compactor.finish(&mut sink);
512
513 assert_eq!(
514 format!("{:?}", ref_acc.chart()),
515 format!("{:?}", sink.acc.chart()),
516 "chart state must be identical whether Log events are present or filtered"
517 );
518 }
519
520 #[test]
523 fn equivalence_strategy_accumulator() {
524 use navi_strategy::{StrategyAccumulator, StrategyEvent};
525
526 let events_with_log = vec![
527 Event::HistoryEnd,
528 bar_start(0, T0, BarState::RealtimeNew),
529 Event::Strategy(StrategyEvent::EquitySnapshot {
530 equity: 1000.0,
531 buy_hold_equity: 950.0,
532 }),
533 Event::Log(LogEvent {
534 level: LogLevel::Info,
535 message: "noise".into(),
536 }),
537 Event::BarEnd,
538 ];
539 let events_without_log: Vec<Event> = events_with_log
540 .iter()
541 .filter(|e| !matches!(e, Event::Log(_)))
542 .cloned()
543 .collect();
544
545 let mut ref_acc = StrategyAccumulator::new();
547 for event in events_without_log {
548 if let Event::Strategy(e) = event {
549 ref_acc.apply(e);
550 }
551 }
552
553 struct StrategySink {
555 acc: StrategyAccumulator,
556 }
557 impl EventCompactionSink for StrategySink {
558 fn on_event(&mut self, event: Event) {
559 if let Event::Strategy(e) = event {
560 self.acc.apply(e);
561 }
562 }
563 fn on_realtime_confirmed(&mut self, _: RealtimeConfirmedInfo) {}
564 }
565 let mut sink = StrategySink {
566 acc: StrategyAccumulator::new(),
567 };
568 let mut compactor = EventCompactor::new(EventCompactionRules {
569 filter_log: true,
570 ..Default::default()
571 });
572 for event in events_with_log {
573 compactor.push(event, &mut sink);
574 }
575 compactor.finish(&mut sink);
576
577 assert_eq!(
578 format!("{:?}", ref_acc.report()),
579 format!("{:?}", sink.acc.report()),
580 "strategy report must be identical whether Log events are present or filtered"
581 );
582 }
583}