bot_core/events.rs
1//! Canonical events delivered by the engine to strategies.
2//!
3//! Events are the read-side API for strategies. Market polling, fill polling,
4//! account snapshots, and exchange health changes are normalized into this enum
5//! before strategy code sees them.
6//!
7//! # Example
8//!
9//! ```
10//! use bot_core::{Event, ExchangeId, InstrumentId, Price, QuoteEvent};
11//! use rust_decimal::Decimal;
12//!
13//! let event = Event::Quote(QuoteEvent {
14//! exchange: ExchangeId::new("hyperliquid"),
15//! instrument: InstrumentId::new("BTC-PERP"),
16//! bid: Price::new(Decimal::new(64_990, 0)),
17//! ask: Price::new(Decimal::new(65_010, 0)),
18//! ts: 1_700_000_000_000,
19//! });
20//!
21//! assert_eq!(event.instrument().unwrap().as_str(), "BTC-PERP");
22//! assert_eq!(event.ts(), 1_700_000_000_000);
23//! ```
24
25use crate::types::*;
26use rust_decimal::Decimal;
27use serde::{Deserialize, Serialize};
28
29/// Events that strategies can receive.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum Event {
32 /// Best bid/ask quote update.
33 Quote(QuoteEvent),
34 /// Funding rate update for a perpetual market.
35 FundingRate(FundingRateEvent),
36
37 /// Exchange accepted an order.
38 OrderAccepted(OrderAcceptedEvent),
39 /// Exchange or local validation rejected an order.
40 OrderRejected(OrderRejectedEvent),
41 /// An order received a partial or full fill.
42 OrderFilled(OrderFilledEvent),
43 /// An order reached the fully-filled terminal state.
44 OrderCompleted(OrderCompletedEvent),
45 /// An order reached the canceled terminal state.
46 OrderCanceled(OrderCanceledEvent),
47
48 /// Exchange health moved between active and halted states.
49 ExchangeStateChanged(ExchangeStateChangedEvent),
50}
51
52impl Event {
53 /// Get the timestamp of this event
54 pub fn ts(&self) -> i64 {
55 match self {
56 Event::Quote(e) => e.ts,
57 Event::FundingRate(e) => e.ts,
58 Event::OrderAccepted(e) => e.ts,
59 Event::OrderRejected(e) => e.ts,
60 Event::OrderFilled(e) => e.ts,
61 Event::OrderCompleted(e) => e.ts,
62 Event::OrderCanceled(e) => e.ts,
63 Event::ExchangeStateChanged(e) => e.ts,
64 }
65 }
66
67 /// Get the instrument ID if applicable
68 pub fn instrument(&self) -> Option<&InstrumentId> {
69 match self {
70 Event::Quote(e) => Some(&e.instrument),
71 Event::FundingRate(e) => Some(&e.instrument),
72 Event::OrderAccepted(e) => Some(&e.instrument),
73 Event::OrderRejected(e) => Some(&e.instrument),
74 Event::OrderFilled(e) => Some(&e.instrument),
75 Event::OrderCompleted(e) => Some(&e.instrument),
76 Event::OrderCanceled(e) => Some(&e.instrument),
77 Event::ExchangeStateChanged(_) => None,
78 }
79 }
80}
81
82// =============================================================================
83// Market Events
84// =============================================================================
85
86/// Quote update (bid/ask)
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct QuoteEvent {
89 /// Exchange that produced the quote.
90 pub exchange: ExchangeId,
91 /// Instrument whose book changed.
92 pub instrument: InstrumentId,
93 /// Best bid price.
94 pub bid: Price,
95 /// Best ask price.
96 pub ask: Price,
97 /// Event timestamp in milliseconds since the Unix epoch.
98 pub ts: i64,
99}
100
101impl QuoteEvent {
102 /// Return the arithmetic midpoint between bid and ask.
103 ///
104 /// # Example
105 ///
106 /// ```
107 /// use bot_core::{ExchangeId, InstrumentId, Price, QuoteEvent};
108 /// use rust_decimal::Decimal;
109 ///
110 /// let quote = QuoteEvent {
111 /// exchange: ExchangeId::new("hyperliquid"),
112 /// instrument: InstrumentId::new("ETH-PERP"),
113 /// bid: Price::new(Decimal::new(2_000, 0)),
114 /// ask: Price::new(Decimal::new(2_002, 0)),
115 /// ts: 1,
116 /// };
117 ///
118 /// assert_eq!(quote.mid().value(), Decimal::new(2_001, 0));
119 /// ```
120 pub fn mid(&self) -> Price {
121 Price((self.bid.0 + self.ask.0) / Decimal::TWO)
122 }
123}
124
125/// Funding rate changed
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct FundingRateEvent {
128 /// Exchange that reported the funding rate.
129 pub exchange: ExchangeId,
130 /// Perpetual instrument receiving the update.
131 pub instrument: InstrumentId,
132 /// Funding rate as a decimal fraction.
133 pub rate: Decimal,
134 /// Event timestamp in milliseconds since the Unix epoch.
135 pub ts: i64,
136}
137
138// =============================================================================
139// Execution Events
140// =============================================================================
141
142/// Order accepted by exchange
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct OrderAcceptedEvent {
145 /// Exchange that accepted the order.
146 pub exchange: ExchangeId,
147 /// Instrument traded by the accepted order.
148 pub instrument: InstrumentId,
149 /// Client order ID generated by the bot.
150 pub client_id: ClientOrderId,
151 /// Exchange order ID when the adapter receives one.
152 pub exchange_order_id: Option<ExchangeOrderId>,
153 /// Event timestamp in milliseconds since the Unix epoch.
154 pub ts: i64,
155}
156
157/// Order rejected by exchange (or locally by engine)
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct OrderRejectedEvent {
160 /// Exchange or adapter that rejected the order.
161 pub exchange: ExchangeId,
162 /// Instrument targeted by the rejected order.
163 pub instrument: InstrumentId,
164 /// Client order ID generated by the bot.
165 pub client_id: ClientOrderId,
166 /// Human-readable rejection reason.
167 pub reason: String,
168 /// Event timestamp in milliseconds since the Unix epoch.
169 pub ts: i64,
170}
171
172/// Order filled (partial or full) - derived from userFills
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct OrderFilledEvent {
175 /// Exchange that reported the fill.
176 pub exchange: ExchangeId,
177 /// Instrument that filled.
178 pub instrument: InstrumentId,
179 /// Client order ID associated with the fill.
180 pub client_id: ClientOrderId,
181 /// Exchange trade ID or stable derived fill key.
182 pub trade_id: TradeId,
183 /// Side of the filled order.
184 pub side: OrderSide,
185 /// Execution price.
186 pub price: Price,
187 /// Gross quantity filled (as reported by exchange)
188 pub qty: Qty,
189 /// Net quantity received/spent after fee deduction.
190 /// For spot BUY: qty - fee (if fee is in base asset)
191 /// For spot SELL: qty (fee is in quote asset)
192 /// For perps: same as qty (fees don't affect position size)
193 pub net_qty: Qty,
194 /// Fee paid for this fill.
195 pub fee: Fee,
196 /// Event timestamp in milliseconds since the Unix epoch.
197 pub ts: i64,
198}
199
200/// Order completed (terminal: fully filled)
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct OrderCompletedEvent {
203 /// Exchange that reported completion.
204 pub exchange: ExchangeId,
205 /// Instrument that completed.
206 pub instrument: InstrumentId,
207 /// Client order ID generated by the bot.
208 pub client_id: ClientOrderId,
209 /// Total filled quantity.
210 pub filled_qty: Qty,
211 /// Average fill price if known.
212 pub avg_fill_px: Option<Price>,
213 /// Event timestamp in milliseconds since the Unix epoch.
214 pub ts: i64,
215}
216
217/// Order canceled
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct OrderCanceledEvent {
220 /// Exchange that canceled the order.
221 pub exchange: ExchangeId,
222 /// Instrument whose order was canceled.
223 pub instrument: InstrumentId,
224 /// Client order ID generated by the bot.
225 pub client_id: ClientOrderId,
226 /// Optional cancellation reason reported by adapter or exchange.
227 pub reason: Option<String>,
228 /// Event timestamp in milliseconds since the Unix epoch.
229 pub ts: i64,
230}
231
232// =============================================================================
233// System Events
234// =============================================================================
235
236/// Exchange state changed (Active <-> Halted)
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ExchangeStateChangedEvent {
239 /// Exchange instance whose health changed.
240 pub exchange: ExchangeId,
241 /// Previous exchange health state.
242 pub old_state: ExchangeHealth,
243 /// New exchange health state.
244 pub new_state: ExchangeHealth,
245 /// Human-readable reason for the transition.
246 pub reason: String,
247 /// Event timestamp in milliseconds since the Unix epoch.
248 pub ts: i64,
249}