atelier_data 0.0.15

Data Artifacts and I/O for the atelier-rs engine
//! Liquidation events: data model, builder, and I/O.
//!
//! Liquidations occur when a trader's collateral falls below the
//! maintenance margin requirement, forcing their position to be
//! automatically closed at market prices.

pub mod io;

use rand::prelude::IndexedRandom;
use rand::{Rng, rng};
use std::time::{SystemTime, UNIX_EPOCH};

/// A single forced-liquidation event observed on an exchange.
///
/// When a trader's margin falls below the maintenance requirement the
/// exchange force-closes their position at market prices.  This struct
/// is the **exchange-agnostic** normalised representation — exchange-
/// specific wire formats are mapped into `Liquidation` by the
/// per-exchange client implementations.
#[derive(Debug, Clone)]
pub struct Liquidation {
    /// Timestamp when the liquidation occurred (Unix ms).
    pub liquidation_ts: u64,
    /// Trading pair symbol (e.g. `"BTCUSDT"`).
    pub symbol: String,
    /// Side being liquidated: `"buy"` or `"sell"`.
    pub side: String,
    /// Liquidated quantity in base-currency units.
    pub amount: f64,
    /// Bankruptcy / fill price in quote-currency units.
    pub price: f64,
    /// Exchange name (e.g. `"bybit"`, `"binance"`).
    pub exchange: String,
}

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

    /// Generate a random `Liquidation` for testing and simulation.
    ///
    /// Produces a liquidation with the current wall-clock timestamp,
    /// a randomly chosen side and exchange, and price / amount drawn
    /// from uniform distributions.
    pub fn random() -> Self {
        let r_liquidation_ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let sides = ["buy", "sell"];
        let exchanges = ["bybit", "kraken", "coinbase", "binance"];
        let mut rng = rng();
        let r_symbol = "btcusdt".to_string();

        let r_side = sides
            .choose(&mut rng)
            .expect("Error in random side choice")
            .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();

        Self {
            liquidation_ts: r_liquidation_ts,
            symbol: r_symbol,
            side: r_side,
            amount: r_amount,
            price: r_price,
            exchange: r_exchange,
        }
    }
}

/// Builder for constructing a [`Liquidation`] with validated fields.
///
/// Every field is required.  Calling [`build()`](Self::build) with any
/// field missing returns `Err(String)` naming the absent field.
#[derive(Debug, Clone)]
pub struct LiquidationBuilder {
    pub liquidation_ts: Option<u64>,
    pub symbol: Option<String>,
    pub side: Option<String>,
    pub amount: Option<f64>,
    pub price: Option<f64>,
    pub exchange: Option<String>,
}

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

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

    /// Set the liquidation timestamp (Unix ms).
    pub fn ts(mut self, ts: u64) -> Self {
        self.liquidation_ts = Some(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 side being liquidated (`"buy"` or `"sell"`).
    pub fn side(mut self, side: String) -> Self {
        self.side = Some(side);
        self
    }

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

    /// Set the bankruptcy / fill 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
    }

    /// Consume the builder and produce a [`Liquidation`].
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if any required field is missing.
    pub fn build(self) -> Result<Liquidation, String> {
        let liquidation_ts = self.liquidation_ts.ok_or("Missing 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")?;

        Ok(Liquidation {
            liquidation_ts,
            symbol,
            side,
            amount,
            price,
            exchange,
        })
    }
}