atelier_data 0.0.15

Data Artifacts and I/O for the atelier-rs engine
use crate::orderbooks::delta::NormalizedDelta;
use serde::Deserialize;

/// Top-level `book` channel message from Kraken WebSocket v2.
///
/// ```json
/// {
///   "channel": "book",
///   "type": "snapshot",
///   "data": [{
///     "symbol": "BTC/USD",
///     "bids": [{"price": 21921.73, "qty": 0.063}],
///     "asks": [{"price": 21922.00, "qty": 0.500}],
///     "checksum": 2439117997,
///     "timestamp": "2023-09-26T16:49:20.962586Z"
///   }]
/// }
/// ```
#[derive(Deserialize, Debug, Clone)]
pub struct KrakenBookResponse {
    pub channel: String,
    /// `"snapshot"` or `"update"`.
    #[serde(rename = "type")]
    pub ty: String,
    pub data: Vec<KrakenBookData>,
}

/// A single book snapshot/update payload within the `data` array.
#[derive(Deserialize, Debug, Clone)]
pub struct KrakenBookData {
    pub symbol: String,
    pub bids: Vec<KrakenPriceLevel>,
    pub asks: Vec<KrakenPriceLevel>,
    /// CRC32 checksum of top-10 bids/asks for integrity validation.
    #[serde(default)]
    pub checksum: u64,
    pub timestamp: String,
}

/// Individual price level in a Kraken book message.
///
/// Kraken sends `price` and `qty` as JSON numbers (floats).
#[derive(Deserialize, Debug, Clone)]
pub struct KrakenPriceLevel {
    pub price: f64,
    pub qty: f64,
}

impl KrakenBookResponse {
    /// Convert to exchange-agnostic [`NormalizedDelta`] for the first data entry.
    ///
    /// Kraken book messages typically contain a single data entry per symbol.
    pub fn to_normalized(&self) -> Option<NormalizedDelta> {
        let data = self.data.first()?;

        let bids: Vec<(String, String)> = data
            .bids
            .iter()
            .map(|l| (l.price.to_string(), l.qty.to_string()))
            .collect();

        let asks: Vec<(String, String)> = data
            .asks
            .iter()
            .map(|l| (l.price.to_string(), l.qty.to_string()))
            .collect();

        Some(NormalizedDelta {
            symbol: data.symbol.clone(),
            bids,
            asks,
            update_id: data.checksum,
            sequence: data.checksum,
            is_snapshot: self.ty == "snapshot",
        })
    }
}