1use 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#[derive(Debug, Deserialize)]
127pub struct TradesRequest {
128 pub venue: String,
130 #[serde(default = "default_pair")]
132 pub pair: String,
133 #[serde(default = "default_trades_limit")]
135 pub limit: u32,
136}
137
138pub async fn handle_trades(Json(req): Json<TradesRequest>) -> impl IntoResponse {
140 let registry = match VenueRegistry::load() {
141 Ok(r) => r,
142 Err(e) => {
143 return (
144 StatusCode::INTERNAL_SERVER_ERROR,
145 Json(serde_json::json!({ "error": format!("Registry error: {e}") })),
146 )
147 .into_response();
148 }
149 };
150
151 let exchange = match registry.create_exchange_client(&req.venue) {
152 Ok(c) => c,
153 Err(e) => {
154 return (
155 StatusCode::BAD_REQUEST,
156 Json(serde_json::json!({ "error": e.to_string() })),
157 )
158 .into_response();
159 }
160 };
161
162 let pair = exchange.format_pair(&req.pair);
163 match exchange.fetch_recent_trades(&pair, req.limit).await {
164 Ok(trades) => {
165 let json_trades: Vec<serde_json::Value> = trades
166 .iter()
167 .map(|t| {
168 serde_json::json!({
169 "price": t.price,
170 "quantity": t.quantity,
171 "quote_quantity": t.quote_quantity,
172 "timestamp_ms": t.timestamp_ms,
173 "side": match t.side {
174 crate::market::TradeSide::Buy => "buy",
175 crate::market::TradeSide::Sell => "sell",
176 },
177 "id": t.id,
178 })
179 })
180 .collect();
181 Json(serde_json::json!({
182 "venue": req.venue,
183 "pair": pair,
184 "trades": json_trades,
185 }))
186 .into_response()
187 }
188 Err(e) => (
189 StatusCode::INTERNAL_SERVER_ERROR,
190 Json(serde_json::json!({ "error": e.to_string() })),
191 )
192 .into_response(),
193 }
194}
195
196#[derive(Debug, Deserialize)]
202pub struct OhlcRequest {
203 pub venue: String,
205 #[serde(default = "default_pair")]
207 pub pair: String,
208 #[serde(default = "default_interval")]
210 pub interval: String,
211 #[serde(default = "default_ohlc_limit")]
213 pub limit: u32,
214}
215
216fn default_interval() -> String {
217 "1h".to_string()
218}
219
220fn default_ohlc_limit() -> u32 {
221 100
222}
223
224pub async fn handle_ohlc(Json(req): Json<OhlcRequest>) -> impl IntoResponse {
226 let registry = match VenueRegistry::load() {
227 Ok(r) => r,
228 Err(e) => {
229 return (
230 StatusCode::INTERNAL_SERVER_ERROR,
231 Json(serde_json::json!({ "error": format!("Registry error: {e}") })),
232 )
233 .into_response();
234 }
235 };
236
237 let exchange = match registry.create_exchange_client(&req.venue) {
238 Ok(c) => c,
239 Err(e) => {
240 return (
241 StatusCode::BAD_REQUEST,
242 Json(serde_json::json!({ "error": e.to_string() })),
243 )
244 .into_response();
245 }
246 };
247
248 let pair = exchange.format_pair(&req.pair);
249 match exchange.fetch_ohlc(&pair, &req.interval, req.limit).await {
250 Ok(candles) => {
251 let json_candles: Vec<serde_json::Value> = candles
252 .iter()
253 .map(|c| {
254 serde_json::json!({
255 "open_time": c.open_time,
256 "open": c.open,
257 "high": c.high,
258 "low": c.low,
259 "close": c.close,
260 "volume": c.volume,
261 "close_time": c.close_time,
262 })
263 })
264 .collect();
265 Json(serde_json::json!({
266 "venue": req.venue,
267 "pair": pair,
268 "interval": req.interval,
269 "candles": json_candles,
270 }))
271 .into_response()
272 }
273 Err(e) => (
274 StatusCode::INTERNAL_SERVER_ERROR,
275 Json(serde_json::json!({ "error": e.to_string() })),
276 )
277 .into_response(),
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use axum::http::StatusCode;
285
286 #[test]
287 fn test_snapshot_request_empty_pair() {
288 let json = serde_json::json!({"venue": "binance", "pair": ""});
289 let req: SnapshotRequest = serde_json::from_value(json).unwrap();
290 assert_eq!(req.pair, "");
291 }
292
293 #[test]
294 fn test_snapshot_request_large_limit() {
295 let json = serde_json::json!({"venue": "x", "trades_limit": 1000});
296 let req: SnapshotRequest = serde_json::from_value(json).unwrap();
297 assert_eq!(req.trades_limit, 1000);
298 }
299
300 #[test]
301 fn test_deserialize_full() {
302 let json = serde_json::json!({
303 "venue": "binance",
304 "pair": "USDC",
305 "trades_limit": 20
306 });
307 let req: SnapshotRequest = serde_json::from_value(json).unwrap();
308 assert_eq!(req.venue, "binance");
309 assert_eq!(req.pair, "USDC");
310 assert_eq!(req.trades_limit, 20);
311 }
312
313 #[test]
314 fn test_deserialize_minimal() {
315 let json = serde_json::json!({
316 "venue": "mexc"
317 });
318 let req: SnapshotRequest = serde_json::from_value(json).unwrap();
319 assert_eq!(req.venue, "mexc");
320 assert_eq!(req.pair, "BTC");
321 assert_eq!(req.trades_limit, 50);
322 }
323
324 #[test]
325 fn test_defaults() {
326 assert_eq!(default_pair(), "BTC");
327 assert_eq!(default_trades_limit(), 50);
328 }
329
330 #[tokio::test]
331 async fn test_handle_unknown_venue() {
332 let req = SnapshotRequest {
333 venue: "nonexistent_venue_xyz".to_string(),
334 pair: "BTC".to_string(),
335 trades_limit: 50,
336 };
337 let response = handle(Json(req)).await.into_response();
338 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
339 }
340
341 #[test]
342 fn test_snapshot_request_deserialization_with_defaults() {
343 let json = serde_json::json!({"venue": "kraken"});
345 let req: SnapshotRequest = serde_json::from_value(json).unwrap();
346 assert_eq!(req.venue, "kraken");
347 assert_eq!(req.pair, "BTC");
348 assert_eq!(req.trades_limit, 50);
349 }
350
351 #[test]
352 fn test_trades_request_deserialization() {
353 let json = serde_json::json!({
354 "venue": "binance",
355 "pair": "USDC",
356 "limit": 25
357 });
358 let req: TradesRequest = serde_json::from_value(json).unwrap();
359 assert_eq!(req.venue, "binance");
360 assert_eq!(req.pair, "USDC");
361 assert_eq!(req.limit, 25);
362 }
363
364 #[test]
365 fn test_trades_request_defaults() {
366 let json = serde_json::json!({"venue": "kraken"});
367 let req: TradesRequest = serde_json::from_value(json).unwrap();
368 assert_eq!(req.venue, "kraken");
369 assert_eq!(req.pair, "BTC");
370 assert_eq!(req.limit, 50);
371 }
372
373 #[test]
374 fn test_ohlc_request_deserialization() {
375 let json = serde_json::json!({
376 "venue": "binance",
377 "pair": "ETH",
378 "interval": "4h",
379 "limit": 200
380 });
381 let req: OhlcRequest = serde_json::from_value(json).unwrap();
382 assert_eq!(req.venue, "binance");
383 assert_eq!(req.pair, "ETH");
384 assert_eq!(req.interval, "4h");
385 assert_eq!(req.limit, 200);
386 }
387
388 #[test]
389 fn test_ohlc_request_defaults() {
390 let json = serde_json::json!({"venue": "mexc"});
391 let req: OhlcRequest = serde_json::from_value(json).unwrap();
392 assert_eq!(req.venue, "mexc");
393 assert_eq!(req.pair, "BTC");
394 assert_eq!(req.interval, "1h");
395 assert_eq!(req.limit, 100);
396 }
397
398 #[test]
399 fn test_snapshot_request_debug() {
400 let req = SnapshotRequest {
401 venue: "test".to_string(),
402 pair: "ETH".to_string(),
403 trades_limit: 100,
404 };
405 let debug = format!("{:?}", req);
406 assert!(debug.contains("SnapshotRequest"));
407 }
408
409 #[tokio::test]
410 async fn test_handle_valid_venue_graceful_failure() {
411 let req = SnapshotRequest {
415 venue: "binance".to_string(),
416 pair: "BTC".to_string(),
417 trades_limit: 5,
418 };
419 let response = handle(Json(req)).await.into_response();
420 let status = response.status();
422 assert!(
423 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
424 "Expected 200 or 500, got {}",
425 status
426 );
427
428 if status == StatusCode::OK {
429 let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
430 .await
431 .unwrap();
432 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
433 assert_eq!(json["venue"], "binance");
434 assert!(json["pair"].is_string());
435 }
437 }
438
439 #[tokio::test]
440 async fn test_handle_multiple_venues() {
441 for venue in &["mexc", "okx", "bybit", "coinbase"] {
443 let req = SnapshotRequest {
444 venue: venue.to_string(),
445 pair: "ETH".to_string(),
446 trades_limit: 5,
447 };
448 let response = handle(Json(req)).await.into_response();
449 let status = response.status();
450 assert!(
451 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
452 "Venue {} returned unexpected status {}",
453 venue,
454 status
455 );
456 }
457 }
458
459 #[tokio::test]
464 async fn test_handle_trades_unknown_venue() {
465 let req = TradesRequest {
466 venue: "nonexistent_venue_xyz".to_string(),
467 pair: "BTC".to_string(),
468 limit: 50,
469 };
470 let response = handle_trades(Json(req)).await.into_response();
471 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
472 let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
473 .await
474 .unwrap();
475 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
476 assert!(json["error"].as_str().unwrap().contains("Unknown venue"));
477 }
478
479 #[tokio::test]
480 async fn test_handle_trades_valid_venue() {
481 let req = TradesRequest {
482 venue: "binance".to_string(),
483 pair: "BTC".to_string(),
484 limit: 5,
485 };
486 let response = handle_trades(Json(req)).await.into_response();
487 let status = response.status();
488 assert!(
490 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
491 "Expected 200 or 500, got {}",
492 status
493 );
494 }
495
496 #[tokio::test]
501 async fn test_handle_ohlc_unknown_venue() {
502 let req = OhlcRequest {
503 venue: "nonexistent_venue_xyz".to_string(),
504 pair: "BTC".to_string(),
505 interval: "1h".to_string(),
506 limit: 100,
507 };
508 let response = handle_ohlc(Json(req)).await.into_response();
509 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
510 let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
511 .await
512 .unwrap();
513 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
514 assert!(json["error"].as_str().unwrap().contains("Unknown venue"));
515 }
516
517 #[tokio::test]
518 async fn test_handle_ohlc_valid_venue() {
519 let req = OhlcRequest {
520 venue: "binance".to_string(),
521 pair: "BTC".to_string(),
522 interval: "1h".to_string(),
523 limit: 5,
524 };
525 let response = handle_ohlc(Json(req)).await.into_response();
526 let status = response.status();
527 assert!(
528 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
529 "Expected 200 or 500, got {}",
530 status
531 );
532 }
533
534 #[test]
535 fn test_trades_request_debug() {
536 let req = TradesRequest {
537 venue: "test".to_string(),
538 pair: "ETH".to_string(),
539 limit: 10,
540 };
541 let debug = format!("{:?}", req);
542 assert!(debug.contains("TradesRequest"));
543 }
544
545 #[test]
546 fn test_ohlc_request_debug() {
547 let req = OhlcRequest {
548 venue: "test".to_string(),
549 pair: "ETH".to_string(),
550 interval: "4h".to_string(),
551 limit: 50,
552 };
553 let debug = format!("{:?}", req);
554 assert!(debug.contains("OhlcRequest"));
555 }
556
557 #[tokio::test]
558 async fn test_handle_trades_multiple_venues() {
559 for venue in &["mexc", "okx", "bybit"] {
560 let req = TradesRequest {
561 venue: venue.to_string(),
562 pair: "BTC".to_string(),
563 limit: 3,
564 };
565 let response = handle_trades(Json(req)).await.into_response();
566 let status = response.status();
567 assert!(
568 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
569 "Venue {} trades returned unexpected status {}",
570 venue,
571 status
572 );
573 }
574 }
575
576 #[tokio::test]
577 async fn test_handle_ohlc_multiple_venues() {
578 for venue in &["mexc", "okx", "bybit"] {
579 let req = OhlcRequest {
580 venue: venue.to_string(),
581 pair: "BTC".to_string(),
582 interval: "1h".to_string(),
583 limit: 3,
584 };
585 let response = handle_ohlc(Json(req)).await.into_response();
586 let status = response.status();
587 assert!(
588 status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
589 "Venue {} ohlc returned unexpected status {}",
590 venue,
591 status
592 );
593 }
594 }
595}