bot-core 0.1.0

Core types, events, commands, and traits for the trading bot
Documentation
//! Exchange adapter trait and exchange-facing DTOs.
//!
//! Adapters implement [`Exchange`] to translate canonical bot commands into
//! concrete exchange API calls. The engine owns retries, state transitions, and
//! event generation; adapters return structured results and errors.
//!
//! # Example
//!
//! ```
//! use bot_core::{
//!     ClientOrderId, InstrumentId, MarketIndex, OrderInput, OrderSide, Price, Qty, TimeInForce,
//! };
//! use rust_decimal::Decimal;
//!
//! let input = OrderInput {
//!     instrument: InstrumentId::new("BTC-PERP"),
//!     market_index: MarketIndex::new(0),
//!     client_id: ClientOrderId::new("client-1"),
//!     side: OrderSide::Buy,
//!     price: Price::new(Decimal::new(65_000, 0)),
//!     qty: Qty::new(Decimal::new(1, 3)),
//!     tif: TimeInForce::Gtc,
//!     post_only: true,
//!     reduce_only: false,
//! };
//!
//! assert!(input.post_only);
//! ```

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

/// Input parameters for placing an order.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderInput {
    /// Canonical instrument ID used by the engine.
    pub instrument: InstrumentId,
    /// Exchange-specific market index or encoded asset ID.
    pub market_index: MarketIndex,
    /// Client order ID generated by the bot.
    pub client_id: ClientOrderId,
    /// Buy or sell side.
    pub side: OrderSide,
    /// Limit price.
    pub price: Price,
    /// Order quantity.
    pub qty: Qty,
    /// Time-in-force instruction.
    pub tif: TimeInForce,
    /// Whether the exchange must reject the order if it would take liquidity.
    pub post_only: bool,
    /// Whether the order may only reduce an existing position.
    pub reduce_only: bool,
}

/// Errors returned by exchange operations.
#[derive(Debug, Error, Clone)]
pub enum ExchangeError {
    /// HTTP status or response-level failure that does not fit a narrower case.
    #[error("HTTP error: {0}")]
    Http(String),

    /// Network transport failure such as DNS, connection reset, or TLS failure.
    #[error("Network error: {0}")]
    Network(String),

    /// Exchange rejected the request because a rate limit was hit.
    #[error("Rate limited (429)")]
    RateLimited,

    /// Exchange action budget is too low for the requested batch.
    #[error("Would exceed user action limit (needed {needed}, retry after {retry_after_ms}ms)")]
    WouldExceedUserActionLimit {
        /// Milliseconds to wait before retrying.
        retry_after_ms: u64,
        /// Number of action credits the attempted operation needed.
        needed: u32,
    },

    /// Exchange is temporarily unavailable.
    #[error("Exchange unavailable (502/503)")]
    Unavailable,

    /// Request exceeded its configured deadline.
    #[error("Request timeout")]
    Timeout,

    /// Exchange accepted the request shape but rejected the order semantics.
    #[error("Order rejected: {0}")]
    Rejected(String),

    /// Adapter could not parse an exchange response.
    #[error("Parse error: {0}")]
    Parse(String),

    /// Request signing failed before submission.
    #[error("Signing error: {0}")]
    Signing(String),

    /// Adapter was configured with invalid or missing settings.
    #[error("Configuration error: {0}")]
    Configuration(String),

    /// Fallback for failures that should be made more specific when possible.
    #[error("Unknown error: {0}")]
    Unknown(String),
}

impl ExchangeError {
    /// Is this error transient (should retry)?
    pub fn is_transient(&self) -> bool {
        matches!(
            self,
            Self::RateLimited
                | Self::WouldExceedUserActionLimit { .. }
                | Self::Unavailable
                | Self::Timeout
                | Self::Network(_)
        )
    }

    /// Is this a 502 (exchange halted)?
    pub fn is_502(&self) -> bool {
        matches!(self, Self::Unavailable)
    }
}

/// Result of placing an order
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlaceOrderResult {
    /// Order accepted by exchange
    Accepted {
        /// Exchange-assigned order ID, if the API returned one.
        exchange_order_id: Option<ExchangeOrderId>,
        /// For IOC orders that are immediately filled, includes fill info
        filled_qty: Option<Qty>,
        /// Average execution price for immediately filled quantity.
        avg_fill_px: Option<Price>,
    },
    /// Order rejected by exchange
    Rejected {
        /// Human-readable rejection reason.
        reason: String,
    },
}

/// A fill from userFills
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fill {
    /// Trade ID (from exchange, or derived stable key)
    pub trade_id: TradeId,
    /// Client order ID (if returned by exchange)
    pub client_id: Option<ClientOrderId>,
    /// Exchange order ID
    pub exchange_order_id: Option<ExchangeOrderId>,
    /// Instrument
    pub instrument: InstrumentId,
    /// Side
    pub side: OrderSide,
    /// Fill price
    pub price: Price,
    /// Fill quantity
    pub qty: Qty,
    /// Fee
    pub fee: Fee,
    /// Exchange timestamp (millis)
    pub ts: i64,
}

/// Account balance reported by an exchange.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountBalance {
    /// Asset whose balance is reported.
    pub asset: AssetId,
    /// Total balance, including unavailable/reserved funds.
    pub total: Decimal,
    /// Balance available for new orders or withdrawals.
    pub available: Decimal,
}

/// Position reported by an exchange.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangePosition {
    /// Instrument for this position.
    pub instrument: InstrumentId,
    /// Signed quantity: positive is long, negative is short.
    pub qty: Decimal, // signed
    /// Average entry price when supplied by the exchange.
    pub avg_entry_px: Option<Price>,
    /// Current unrealized PnL when supplied by the exchange.
    pub unrealized_pnl: Option<Decimal>,
}

/// Exchange adapter trait - what each exchange must implement.
///
/// All methods are async and return Results to handle network errors.
/// The engine calls these methods and translates results into canonical events.
#[async_trait::async_trait]
pub trait Exchange: Send + Sync + 'static {
    /// Get the exchange ID
    fn exchange_id(&self) -> &ExchangeId;

    /// Get the environment (mainnet/testnet)
    fn environment(&self) -> Environment;

    /// Get the exchange instance key
    fn instance(&self) -> ExchangeInstance {
        ExchangeInstance::new(self.exchange_id().clone(), self.environment())
    }

    /// Initialize the exchange connection and perform startup validations.
    /// Called by the runner before entering the main loop.
    /// Implementations can validate: connectivity, vault ownership, account balance, etc.
    async fn init(&self) -> Result<(), ExchangeError> {
        Ok(()) // default no-op for paper/mock exchanges
    }

    // -------------------------------------------------------------------------
    // Write operations (trading)
    // -------------------------------------------------------------------------

    /// Place multiple limit orders in a single batch API call.
    /// Returns a Vec of results, one for each order in the same order as the input.
    async fn place_orders(
        &self,
        orders: &[OrderInput],
    ) -> Result<Vec<PlaceOrderResult>, ExchangeError>;

    /// Cancel an order by client_id or exchange_order_id.
    async fn cancel_order(
        &self,
        instrument: &InstrumentId,
        market_index: &MarketIndex,
        client_id: &ClientOrderId,
        exchange_order_id: Option<&ExchangeOrderId>,
    ) -> Result<(), ExchangeError>;

    /// Cancel all orders for an instrument.
    /// Returns number of orders canceled.
    async fn cancel_all_orders(
        &self,
        instrument: &InstrumentId,
        market_index: &MarketIndex,
    ) -> Result<u32, ExchangeError>;

    // -------------------------------------------------------------------------
    // Read operations (polling)
    // -------------------------------------------------------------------------

    /// Poll user fills since cursor.
    /// This is the primary execution event source.
    /// cursor is opaque string (implementation-specific).
    async fn poll_user_fills(&self, cursor: Option<&str>) -> Result<Vec<Fill>, ExchangeError>;

    /// Poll current quotes/prices.
    async fn poll_quotes(
        &self,
        instruments: &[InstrumentId],
    ) -> Result<Vec<crate::types::Quote>, ExchangeError>;

    /// Poll account state (absolute).
    /// Used for snapshot-based synchronization.
    async fn poll_account_state(&self) -> Result<AccountState, ExchangeError>;
}