Skip to main content

scope/web/api/
exchange.rs

1//! Exchange snapshot API handler.
2//!
3//! POST /api/exchange/snapshot — Fetches full market snapshot (order book, ticker,
4//! recent trades) for a given venue and pair.
5
6use crate::market::VenueRegistry;
7use axum::Json;
8use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use serde::Deserialize;
11
12/// Request body for exchange snapshot.
13#[derive(Debug, Deserialize)]
14pub struct SnapshotRequest {
15    /// Venue ID (e.g., "binance", "mexc").
16    pub venue: String,
17    /// Base token symbol (e.g., "BTC", "USDC").
18    #[serde(default = "default_pair")]
19    pub pair: String,
20    /// Maximum number of recent trades to fetch.
21    #[serde(default = "default_trades_limit")]
22    pub trades_limit: u32,
23}
24
25fn default_pair() -> String {
26    "BTC".to_string()
27}
28
29fn default_trades_limit() -> u32 {
30    50
31}
32
33/// POST /api/exchange/snapshot — Full market snapshot.
34pub async fn handle(Json(req): Json<SnapshotRequest>) -> impl IntoResponse {
35    let registry = match VenueRegistry::load() {
36        Ok(r) => r,
37        Err(e) => {
38            return (
39                StatusCode::INTERNAL_SERVER_ERROR,
40                Json(serde_json::json!({ "error": format!("Registry error: {e}") })),
41            )
42                .into_response();
43        }
44    };
45
46    let exchange = match registry.create_exchange_client(&req.venue) {
47        Ok(c) => c,
48        Err(e) => {
49            return (
50                StatusCode::BAD_REQUEST,
51                Json(serde_json::json!({ "error": e.to_string() })),
52            )
53                .into_response();
54        }
55    };
56
57    let pair = exchange.format_pair(&req.pair);
58    let snapshot = exchange.fetch_market_snapshot(&pair).await;
59
60    let order_book_json = snapshot.order_book.as_ref().map(|book| {
61        serde_json::json!({
62            "pair": book.pair,
63            "best_bid": book.best_bid(),
64            "best_ask": book.best_ask(),
65            "mid_price": book.mid_price(),
66            "spread": book.spread(),
67            "bid_depth": book.bid_depth(),
68            "ask_depth": book.ask_depth(),
69            "bids": book.bids.iter().map(|l| {
70                serde_json::json!({"price": l.price, "quantity": l.quantity, "value": l.value()})
71            }).collect::<Vec<_>>(),
72            "asks": book.asks.iter().map(|l| {
73                serde_json::json!({"price": l.price, "quantity": l.quantity, "value": l.value()})
74            }).collect::<Vec<_>>(),
75        })
76    });
77
78    let ticker_json = snapshot.ticker.as_ref().map(|t| {
79        serde_json::json!({
80            "pair": t.pair,
81            "last_price": t.last_price,
82            "high_24h": t.high_24h,
83            "low_24h": t.low_24h,
84            "volume_24h": t.volume_24h,
85            "quote_volume_24h": t.quote_volume_24h,
86            "best_bid": t.best_bid,
87            "best_ask": t.best_ask,
88        })
89    });
90
91    let trades_json = snapshot.recent_trades.as_ref().map(|trades| {
92        trades
93            .iter()
94            .map(|t| {
95                serde_json::json!({
96                    "price": t.price,
97                    "quantity": t.quantity,
98                    "quote_quantity": t.quote_quantity,
99                    "timestamp_ms": t.timestamp_ms,
100                    "side": match t.side {
101                        crate::market::TradeSide::Buy => "buy",
102                        crate::market::TradeSide::Sell => "sell",
103                    },
104                    "id": t.id,
105                })
106            })
107            .collect::<Vec<_>>()
108    });
109
110    let output = serde_json::json!({
111        "venue": req.venue,
112        "pair": pair,
113        "order_book": order_book_json,
114        "ticker": ticker_json,
115        "recent_trades": trades_json,
116    });
117
118    Json(output).into_response()
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_deserialize_full() {
127        let json = serde_json::json!({
128            "venue": "binance",
129            "pair": "USDC",
130            "trades_limit": 20
131        });
132        let req: SnapshotRequest = serde_json::from_value(json).unwrap();
133        assert_eq!(req.venue, "binance");
134        assert_eq!(req.pair, "USDC");
135        assert_eq!(req.trades_limit, 20);
136    }
137
138    #[test]
139    fn test_deserialize_minimal() {
140        let json = serde_json::json!({
141            "venue": "mexc"
142        });
143        let req: SnapshotRequest = serde_json::from_value(json).unwrap();
144        assert_eq!(req.venue, "mexc");
145        assert_eq!(req.pair, "BTC");
146        assert_eq!(req.trades_limit, 50);
147    }
148
149    #[test]
150    fn test_defaults() {
151        assert_eq!(default_pair(), "BTC");
152        assert_eq!(default_trades_limit(), 50);
153    }
154}