atelier_data 0.0.15

Data Artifacts and I/O for the atelier-rs engine
use rand::prelude::IndexedRandom;
use rand::{Rng, rng};
use std::time::{SystemTime, UNIX_EPOCH};

/// A single public trade observed on an exchange.
///
/// Trades are the most granular market event, recording every
/// fill on the matching engine.  This is the **exchange-agnostic**
/// normalised representation — exchange-specific wire formats are
/// mapped into `Trade` by the per-exchange client implementations.
///
/// Timestamps are in Unix milliseconds as reported by the exchange.
#[derive(Debug, Clone)]
pub struct Trade {
    /// Timestamp when this trade was observed (Unix ms).
    pub trade_ts: u64,
    /// Trading pair symbol (e.g. `"BTCUSDT"`).
    pub symbol: String,
    /// Taker side: `"buy"` or `"sell"`.
    pub side: String,
    /// Filled quantity in base-currency units.
    pub amount: f64,
    /// Execution price in quote-currency units.
    pub price: f64,
    /// Exchange name (e.g. `"bybit"`, `"kraken"`).
    pub exchange: String,
    /// Exchange-assigned trade identifier.
    pub id: String,
}

impl Trade {
    /// Create a new [`TradeBuilder`].
    pub fn builder() -> TradeBuilder {
        TradeBuilder::new()
    }

    /// Generate a random `Trade` for testing and simulation.
    ///
    /// Produces a trade with the current wall-clock timestamp, a
    /// randomly chosen side and exchange, and price / amount drawn
    /// from uniform distributions.  The `symbol` and `id` fields are
    /// left as empty strings.
    pub fn random() -> Self {
        let r_ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let sides = ["buy", "sell"];
        let mut rng = rng();
        let r_side = sides
            .choose(&mut rng)
            .expect("Error in random side choice")
            .to_string();

        let exchanges = ["bybit", "kraken", "coinbase", "binance"];

        let r_symbol = "".to_string();
        let r_amount = rng.random_range(0.01..1.10);
        let r_price = rng.random_range(100_000.0..110_000.0);
        let r_exchange = exchanges
            .choose(&mut rng)
            .expect("Error in random side choice")
            .to_string();
        let r_id = "".to_string();

        Self {
            trade_ts: r_ts,
            symbol: r_symbol,
            side: r_side,
            amount: r_amount,
            price: r_price,
            exchange: r_exchange,
            id: r_id,
        }
    }
}

/// Builder for constructing a [`Trade`] with validated fields.
///
/// Every field is required.  Calling [`build()`](Self::build) with any
/// field missing returns `Err(String)` naming the absent field.
///
/// # Example
///
/// ```rust,ignore
/// let trade = Trade::builder()
///     .trade_ts(1_700_000_000_000)
///     .symbol("BTCUSDT".into())
///     .side("buy".into())
///     .amount(0.5)
///     .price(42_000.0)
///     .exchange("bybit".into())
///     .id("abc123".into())
///     .build()
///     .expect("all fields set");
/// ```
#[derive(Debug, Clone)]
pub struct TradeBuilder {
    pub trade_ts: Option<u64>,
    pub symbol: Option<String>,
    pub side: Option<String>,
    pub amount: Option<f64>,
    pub price: Option<f64>,
    pub exchange: Option<String>,
    pub id: Option<String>,
}

impl Default for TradeBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl TradeBuilder {
    /// Create an empty builder with all fields set to `None`.
    pub fn new() -> Self {
        TradeBuilder {
            trade_ts: None,
            symbol: None,
            side: None,
            amount: None,
            price: None,
            exchange: None,
            id: None,
        }
    }

    /// Set the trade timestamp (Unix ms).
    pub fn trade_ts(mut self, trade_ts: u64) -> Self {
        self.trade_ts = Some(trade_ts);
        self
    }

    /// Set the trading pair symbol (e.g. `"BTCUSDT"`).
    pub fn symbol(mut self, symbol: String) -> Self {
        self.symbol = Some(symbol);
        self
    }

    /// Set the taker side (`"buy"` or `"sell"`).
    pub fn side(mut self, side: String) -> Self {
        self.side = Some(side);
        self
    }

    /// Set the filled quantity in base-currency units.
    pub fn amount(mut self, amount: f64) -> Self {
        self.amount = Some(amount);
        self
    }

    /// Set the execution price in quote-currency units.
    pub fn price(mut self, price: f64) -> Self {
        self.price = Some(price);
        self
    }

    /// Set the exchange name (e.g. `"bybit"`).
    pub fn exchange(mut self, exchange: String) -> Self {
        self.exchange = Some(exchange);
        self
    }

    /// Set the exchange-assigned trade identifier.
    pub fn id(mut self, id: String) -> Self {
        self.id = Some(id);
        self
    }

    /// Consume the builder and produce a [`Trade`].
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if any required field is missing.
    pub fn build(self) -> Result<Trade, String> {
        let trade_ts = self.trade_ts.ok_or("Missing trade_ts")?;
        let symbol = self.symbol.ok_or("Missing symbol")?;
        let side = self.side.ok_or("Missing side")?;
        let amount = self.amount.ok_or("Missing amount")?;
        let price = self.price.ok_or("Missing price")?;
        let exchange = self.exchange.ok_or("Missing exchange")?;
        let id = self.id.ok_or("Missing id")?;

        Ok(Trade {
            trade_ts,
            symbol,
            side,
            amount,
            price,
            exchange,
            id,
        })
    }
}