Skip to main content

hyper_agent_notify/
emitter.rs

1use serde::{Deserialize, Serialize};
2use std::sync::{Arc, Mutex};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case", tag = "type")]
6pub enum TradingEvent {
7    EngineStarted {
8        id: String,
9    },
10    EngineStopped {
11        id: String,
12        reason: String,
13    },
14    TickCompleted {
15        symbol: String,
16        regime: String,
17    },
18    SignalGenerated {
19        signal_id: String,
20        symbol: String,
21        action: String,
22    },
23    OrderExecuted {
24        order_id: String,
25        market: String,
26        side: String,
27        price: f64,
28        size: f64,
29    },
30    OrderBlocked {
31        market: String,
32        reason: String,
33    },
34    RiskAlert {
35        message: String,
36    },
37    CircuitBreakerTripped,
38    AgentAdjustment {
39        changes: String,
40    },
41}
42
43pub trait TradingEventEmitter: Send + Sync {
44    fn emit(&self, event: TradingEvent);
45}
46
47/// Broadcasts events via tokio broadcast channel. Used by daemon SSE.
48pub struct BroadcastEmitter {
49    tx: tokio::sync::broadcast::Sender<TradingEvent>,
50}
51
52impl BroadcastEmitter {
53    pub fn new(tx: tokio::sync::broadcast::Sender<TradingEvent>) -> Self {
54        Self { tx }
55    }
56}
57
58impl TradingEventEmitter for BroadcastEmitter {
59    fn emit(&self, event: TradingEvent) {
60        let _ = self.tx.send(event);
61    }
62}
63
64/// Logs events via tracing. Used by CLI.
65pub struct LogEmitter;
66
67impl TradingEventEmitter for LogEmitter {
68    fn emit(&self, event: TradingEvent) {
69        tracing::info!(event = ?event, "trading event");
70    }
71}
72
73/// Records events in memory. Used by tests.
74pub struct RecordingEmitter {
75    events: Arc<Mutex<Vec<TradingEvent>>>,
76}
77
78impl RecordingEmitter {
79    pub fn new() -> Self {
80        Self {
81            events: Arc::new(Mutex::new(Vec::new())),
82        }
83    }
84
85    pub fn events(&self) -> Vec<TradingEvent> {
86        self.events.lock().unwrap().clone()
87    }
88
89    pub fn len(&self) -> usize {
90        self.events.lock().unwrap().len()
91    }
92}
93
94impl Default for RecordingEmitter {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl TradingEventEmitter for RecordingEmitter {
101    fn emit(&self, event: TradingEvent) {
102        self.events.lock().unwrap().push(event);
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn recording_emitter_captures_events() {
112        let emitter = RecordingEmitter::new();
113        emitter.emit(TradingEvent::EngineStarted { id: "test".into() });
114        emitter.emit(TradingEvent::TickCompleted {
115            symbol: "BTC".into(),
116            regime: "bull".into(),
117        });
118        assert_eq!(emitter.len(), 2);
119    }
120
121    #[test]
122    fn trading_event_serde_roundtrip() {
123        let event = TradingEvent::OrderExecuted {
124            order_id: "o1".into(),
125            market: "BTC-PERP".into(),
126            side: "buy".into(),
127            price: 65000.0,
128            size: 0.01,
129        };
130        let json = serde_json::to_string(&event).unwrap();
131        let back: TradingEvent = serde_json::from_str(&json).unwrap();
132        assert!(matches!(back, TradingEvent::OrderExecuted { .. }));
133    }
134
135    #[test]
136    fn log_emitter_does_not_panic() {
137        let emitter = LogEmitter;
138        emitter.emit(TradingEvent::CircuitBreakerTripped);
139    }
140}