scope/web/api/
exchange.rs1use crate::market::VenueRegistry;
7use axum::Json;
8use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use serde::Deserialize;
11
12#[derive(Debug, Deserialize)]
14pub struct SnapshotRequest {
15 pub venue: String,
17 #[serde(default = "default_pair")]
19 pub pair: String,
20 #[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
33pub 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}