Skip to main content

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}