bot-core 0.1.0

Core types, events, commands, and traits for the trading bot
Documentation
//! Canonical events delivered by the engine to strategies.
//!
//! Events are the read-side API for strategies. Market polling, fill polling,
//! account snapshots, and exchange health changes are normalized into this enum
//! before strategy code sees them.
//!
//! # Example
//!
//! ```
//! use bot_core::{Event, ExchangeId, InstrumentId, Price, QuoteEvent};
//! use rust_decimal::Decimal;
//!
//! let event = Event::Quote(QuoteEvent {
//!     exchange: ExchangeId::new("hyperliquid"),
//!     instrument: InstrumentId::new("BTC-PERP"),
//!     bid: Price::new(Decimal::new(64_990, 0)),
//!     ask: Price::new(Decimal::new(65_010, 0)),
//!     ts: 1_700_000_000_000,
//! });
//!
//! assert_eq!(event.instrument().unwrap().as_str(), "BTC-PERP");
//! assert_eq!(event.ts(), 1_700_000_000_000);
//! ```

use crate::types::*;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

/// Events that strategies can receive.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Event {
    /// Best bid/ask quote update.
    Quote(QuoteEvent),
    /// Funding rate update for a perpetual market.
    FundingRate(FundingRateEvent),

    /// Exchange accepted an order.
    OrderAccepted(OrderAcceptedEvent),
    /// Exchange or local validation rejected an order.
    OrderRejected(OrderRejectedEvent),
    /// An order received a partial or full fill.
    OrderFilled(OrderFilledEvent),
    /// An order reached the fully-filled terminal state.
    OrderCompleted(OrderCompletedEvent),
    /// An order reached the canceled terminal state.
    OrderCanceled(OrderCanceledEvent),

    /// Exchange health moved between active and halted states.
    ExchangeStateChanged(ExchangeStateChangedEvent),
}

impl Event {
    /// Get the timestamp of this event
    pub fn ts(&self) -> i64 {
        match self {
            Event::Quote(e) => e.ts,
            Event::FundingRate(e) => e.ts,
            Event::OrderAccepted(e) => e.ts,
            Event::OrderRejected(e) => e.ts,
            Event::OrderFilled(e) => e.ts,
            Event::OrderCompleted(e) => e.ts,
            Event::OrderCanceled(e) => e.ts,
            Event::ExchangeStateChanged(e) => e.ts,
        }
    }

    /// Get the instrument ID if applicable
    pub fn instrument(&self) -> Option<&InstrumentId> {
        match self {
            Event::Quote(e) => Some(&e.instrument),
            Event::FundingRate(e) => Some(&e.instrument),
            Event::OrderAccepted(e) => Some(&e.instrument),
            Event::OrderRejected(e) => Some(&e.instrument),
            Event::OrderFilled(e) => Some(&e.instrument),
            Event::OrderCompleted(e) => Some(&e.instrument),
            Event::OrderCanceled(e) => Some(&e.instrument),
            Event::ExchangeStateChanged(_) => None,
        }
    }
}

// =============================================================================
// Market Events
// =============================================================================

/// Quote update (bid/ask)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuoteEvent {
    /// Exchange that produced the quote.
    pub exchange: ExchangeId,
    /// Instrument whose book changed.
    pub instrument: InstrumentId,
    /// Best bid price.
    pub bid: Price,
    /// Best ask price.
    pub ask: Price,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

impl QuoteEvent {
    /// Return the arithmetic midpoint between bid and ask.
    ///
    /// # Example
    ///
    /// ```
    /// use bot_core::{ExchangeId, InstrumentId, Price, QuoteEvent};
    /// use rust_decimal::Decimal;
    ///
    /// let quote = QuoteEvent {
    ///     exchange: ExchangeId::new("hyperliquid"),
    ///     instrument: InstrumentId::new("ETH-PERP"),
    ///     bid: Price::new(Decimal::new(2_000, 0)),
    ///     ask: Price::new(Decimal::new(2_002, 0)),
    ///     ts: 1,
    /// };
    ///
    /// assert_eq!(quote.mid().value(), Decimal::new(2_001, 0));
    /// ```
    pub fn mid(&self) -> Price {
        Price((self.bid.0 + self.ask.0) / Decimal::TWO)
    }
}

/// Funding rate changed
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingRateEvent {
    /// Exchange that reported the funding rate.
    pub exchange: ExchangeId,
    /// Perpetual instrument receiving the update.
    pub instrument: InstrumentId,
    /// Funding rate as a decimal fraction.
    pub rate: Decimal,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

// =============================================================================
// Execution Events
// =============================================================================

/// Order accepted by exchange
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderAcceptedEvent {
    /// Exchange that accepted the order.
    pub exchange: ExchangeId,
    /// Instrument traded by the accepted order.
    pub instrument: InstrumentId,
    /// Client order ID generated by the bot.
    pub client_id: ClientOrderId,
    /// Exchange order ID when the adapter receives one.
    pub exchange_order_id: Option<ExchangeOrderId>,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

/// Order rejected by exchange (or locally by engine)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderRejectedEvent {
    /// Exchange or adapter that rejected the order.
    pub exchange: ExchangeId,
    /// Instrument targeted by the rejected order.
    pub instrument: InstrumentId,
    /// Client order ID generated by the bot.
    pub client_id: ClientOrderId,
    /// Human-readable rejection reason.
    pub reason: String,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

/// Order filled (partial or full) - derived from userFills
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderFilledEvent {
    /// Exchange that reported the fill.
    pub exchange: ExchangeId,
    /// Instrument that filled.
    pub instrument: InstrumentId,
    /// Client order ID associated with the fill.
    pub client_id: ClientOrderId,
    /// Exchange trade ID or stable derived fill key.
    pub trade_id: TradeId,
    /// Side of the filled order.
    pub side: OrderSide,
    /// Execution price.
    pub price: Price,
    /// Gross quantity filled (as reported by exchange)
    pub qty: Qty,
    /// Net quantity received/spent after fee deduction.
    /// For spot BUY: qty - fee (if fee is in base asset)
    /// For spot SELL: qty (fee is in quote asset)
    /// For perps: same as qty (fees don't affect position size)
    pub net_qty: Qty,
    /// Fee paid for this fill.
    pub fee: Fee,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

/// Order completed (terminal: fully filled)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderCompletedEvent {
    /// Exchange that reported completion.
    pub exchange: ExchangeId,
    /// Instrument that completed.
    pub instrument: InstrumentId,
    /// Client order ID generated by the bot.
    pub client_id: ClientOrderId,
    /// Total filled quantity.
    pub filled_qty: Qty,
    /// Average fill price if known.
    pub avg_fill_px: Option<Price>,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

/// Order canceled
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderCanceledEvent {
    /// Exchange that canceled the order.
    pub exchange: ExchangeId,
    /// Instrument whose order was canceled.
    pub instrument: InstrumentId,
    /// Client order ID generated by the bot.
    pub client_id: ClientOrderId,
    /// Optional cancellation reason reported by adapter or exchange.
    pub reason: Option<String>,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}

// =============================================================================
// System Events
// =============================================================================

/// Exchange state changed (Active <-> Halted)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeStateChangedEvent {
    /// Exchange instance whose health changed.
    pub exchange: ExchangeId,
    /// Previous exchange health state.
    pub old_state: ExchangeHealth,
    /// New exchange health state.
    pub new_state: ExchangeHealth,
    /// Human-readable reason for the transition.
    pub reason: String,
    /// Event timestamp in milliseconds since the Unix epoch.
    pub ts: i64,
}