1use crate::cli::crawl::{self, Period};
4use crate::market::{
5 BinanceClient, HealthThresholds, MarketSummary, MarketVenue, order_book_from_analytics,
6};
7use crate::web::AppState;
8use axum::Json;
9use axum::extract::State;
10use axum::http::StatusCode;
11use axum::response::IntoResponse;
12use serde::Deserialize;
13use std::sync::Arc;
14
15#[derive(Debug, Deserialize)]
17pub struct MarketRequest {
18 #[serde(default = "default_pair")]
20 pub pair: String,
21 #[serde(default = "default_venue")]
23 pub market_venue: String,
24 #[serde(default = "default_chain")]
26 pub chain: String,
27 #[serde(default = "default_peg")]
29 pub peg: f64,
30 #[serde(default = "default_min_levels")]
32 pub min_levels: usize,
33 #[serde(default = "default_min_depth")]
35 pub min_depth: f64,
36 #[serde(default = "default_peg_range")]
38 pub peg_range: f64,
39}
40
41fn default_pair() -> String {
42 "USDC".to_string()
43}
44fn default_venue() -> String {
45 "binance".to_string()
46}
47fn default_chain() -> String {
48 "ethereum".to_string()
49}
50fn default_peg() -> f64 {
51 1.0
52}
53fn default_min_levels() -> usize {
54 6
55}
56fn default_min_depth() -> f64 {
57 3000.0
58}
59fn default_peg_range() -> f64 {
60 0.001
61}
62
63fn summary_to_json(summary: &MarketSummary) -> serde_json::Value {
65 serde_json::json!({
66 "pair": summary.pair,
67 "peg_target": summary.peg_target,
68 "best_bid": summary.best_bid,
69 "best_ask": summary.best_ask,
70 "mid_price": summary.mid_price,
71 "spread": summary.spread,
72 "volume_24h": summary.volume_24h,
73 "bid_depth": summary.bid_depth,
74 "ask_depth": summary.ask_depth,
75 "bid_outliers": summary.bid_outliers,
76 "ask_outliers": summary.ask_outliers,
77 "healthy": summary.healthy,
78 "checks": summary.checks.iter().map(|c| match c {
79 crate::market::HealthCheck::Pass(msg) => serde_json::json!({"status": "pass", "message": msg}),
80 crate::market::HealthCheck::Fail(msg) => serde_json::json!({"status": "fail", "message": msg}),
81 }).collect::<Vec<_>>(),
82 })
83}
84
85pub async fn handle(
87 State(state): State<Arc<AppState>>,
88 Json(req): Json<MarketRequest>,
89) -> impl IntoResponse {
90 let venue: MarketVenue = match req.market_venue.as_str() {
91 "biconomy" => MarketVenue::Biconomy,
92 "eth" | "ethereum" => MarketVenue::Ethereum,
93 "solana" | "sol" => MarketVenue::Solana,
94 _ => MarketVenue::Binance,
95 };
96
97 let thresholds = HealthThresholds {
98 peg_target: req.peg,
99 peg_range: req.peg_range,
100 min_levels: req.min_levels,
101 min_depth: req.min_depth,
102 min_bid_ask_ratio: 0.2,
103 max_bid_ask_ratio: 5.0,
104 };
105
106 if venue.is_cex() {
107 let pair = venue.format_pair(&req.pair);
108 let client_opt = venue.create_client();
109 let Some(client) = client_opt else {
110 return (
111 StatusCode::BAD_REQUEST,
112 Json(serde_json::json!({ "error": "Failed to create market client" })),
113 )
114 .into_response();
115 };
116
117 match client.fetch_order_book(&pair).await {
118 Ok(book) => {
119 let volume_24h = match venue {
120 MarketVenue::Binance => BinanceClient::default_url()
121 .fetch_24h_volume(&pair)
122 .await
123 .ok()
124 .flatten(),
125 _ => None,
126 };
127 let summary =
128 MarketSummary::from_order_book(&book, req.peg, &thresholds, volume_24h);
129 Json(summary_to_json(&summary)).into_response()
130 }
131 Err(e) => (
132 StatusCode::INTERNAL_SERVER_ERROR,
133 Json(serde_json::json!({ "error": e.to_string() })),
134 )
135 .into_response(),
136 }
137 } else {
138 let venue_chain = match venue {
140 MarketVenue::Ethereum => "ethereum",
141 MarketVenue::Solana => "solana",
142 _ => &req.chain,
143 };
144
145 match crawl::fetch_analytics_for_input(
146 &req.pair,
147 venue_chain,
148 Period::Hour24,
149 10,
150 &state.factory,
151 )
152 .await
153 {
154 Ok(analytics) => {
155 if analytics.dex_pairs.is_empty() {
156 return (
157 StatusCode::NOT_FOUND,
158 Json(serde_json::json!({ "error": "No DEX pairs found" })),
159 )
160 .into_response();
161 }
162 let best_pair = analytics
163 .dex_pairs
164 .iter()
165 .max_by(|a, b| {
166 a.liquidity_usd
167 .partial_cmp(&b.liquidity_usd)
168 .unwrap_or(std::cmp::Ordering::Equal)
169 })
170 .unwrap();
171 let book =
172 order_book_from_analytics(venue_chain, best_pair, &analytics.token.symbol);
173 let summary = MarketSummary::from_order_book(
174 &book,
175 req.peg,
176 &thresholds,
177 Some(best_pair.volume_24h),
178 );
179 Json(summary_to_json(&summary)).into_response()
180 }
181 Err(e) => (
182 StatusCode::INTERNAL_SERVER_ERROR,
183 Json(serde_json::json!({ "error": e.to_string() })),
184 )
185 .into_response(),
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_deserialize_full() {
196 let json = serde_json::json!({
197 "pair": "PUSD",
198 "market_venue": "biconomy",
199 "chain": "polygon",
200 "peg": 1.0,
201 "min_levels": 10,
202 "min_depth": 5000.0,
203 "peg_range": 0.002
204 });
205 let req: MarketRequest = serde_json::from_value(json).unwrap();
206 assert_eq!(req.pair, "PUSD");
207 assert_eq!(req.market_venue, "biconomy");
208 assert_eq!(req.chain, "polygon");
209 assert_eq!(req.peg, 1.0);
210 assert_eq!(req.min_levels, 10);
211 assert_eq!(req.min_depth, 5000.0);
212 assert_eq!(req.peg_range, 0.002);
213 }
214
215 #[test]
216 fn test_deserialize_minimal() {
217 let json = serde_json::json!({});
218 let req: MarketRequest = serde_json::from_value(json).unwrap();
219 assert_eq!(req.pair, "USDC");
220 assert_eq!(req.market_venue, "binance");
221 assert_eq!(req.chain, "ethereum");
222 assert_eq!(req.peg, 1.0);
223 assert_eq!(req.min_levels, 6);
224 assert_eq!(req.min_depth, 3000.0);
225 assert_eq!(req.peg_range, 0.001);
226 }
227
228 #[test]
229 fn test_all_defaults() {
230 assert_eq!(default_pair(), "USDC");
231 assert_eq!(default_venue(), "binance");
232 assert_eq!(default_chain(), "ethereum");
233 assert_eq!(default_peg(), 1.0);
234 assert_eq!(default_min_levels(), 6);
235 assert_eq!(default_min_depth(), 3000.0);
236 assert_eq!(default_peg_range(), 0.001);
237 }
238
239 #[test]
240 fn test_custom_thresholds() {
241 let json = serde_json::json!({
242 "min_levels": 20,
243 "min_depth": 10000.0,
244 "peg_range": 0.005
245 });
246 let req: MarketRequest = serde_json::from_value(json).unwrap();
247 assert_eq!(req.min_levels, 20);
248 assert_eq!(req.min_depth, 10000.0);
249 assert_eq!(req.peg_range, 0.005);
250 assert_eq!(req.pair, "USDC");
252 assert_eq!(req.market_venue, "binance");
253 assert_eq!(req.chain, "ethereum");
254 assert_eq!(req.peg, 1.0);
255 }
256
257 #[tokio::test]
258 async fn test_handle_market_cex() {
259 use crate::chains::DefaultClientFactory;
260 use crate::config::Config;
261 use crate::web::AppState;
262 use axum::extract::State;
263 use axum::response::IntoResponse;
264
265 let config = Config::default();
266 let factory = DefaultClientFactory {
267 chains_config: config.chains.clone(),
268 };
269 let state = std::sync::Arc::new(AppState { config, factory });
270 let req = MarketRequest {
271 pair: "USDC".to_string(),
272 market_venue: "binance".to_string(),
273 chain: "ethereum".to_string(),
274 peg: 1.0,
275 min_levels: 6,
276 min_depth: 3000.0,
277 peg_range: 0.001,
278 };
279 let response = handle(State(state), axum::Json(req)).await.into_response();
280 let status = response.status();
281 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
282 }
283
284 #[tokio::test]
285 async fn test_handle_market_dex() {
286 use crate::chains::DefaultClientFactory;
287 use crate::config::Config;
288 use crate::web::AppState;
289 use axum::extract::State;
290 use axum::response::IntoResponse;
291
292 let config = Config::default();
293 let factory = DefaultClientFactory {
294 chains_config: config.chains.clone(),
295 };
296 let state = std::sync::Arc::new(AppState { config, factory });
297 let req = MarketRequest {
298 pair: "USDC".to_string(),
299 market_venue: "eth".to_string(),
300 chain: "ethereum".to_string(),
301 peg: 1.0,
302 min_levels: 6,
303 min_depth: 3000.0,
304 peg_range: 0.001,
305 };
306 let response = handle(State(state), axum::Json(req)).await.into_response();
307 let status = response.status();
308 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
309 }
310}