1use chrono::{FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc};
31use serde_json::Value;
32use crate::core::types::{Kline, Ticker, OrderBook, OrderBookLevel};
33
34pub type ParseResult<T> = Result<T, String>;
36
37pub struct MoexParser;
39
40impl MoexParser {
41 fn get_block<'a>(response: &'a Value, block_name: &str) -> ParseResult<&'a Value> {
49 response
50 .get(block_name)
51 .ok_or_else(|| format!("Missing '{}' block", block_name))
52 }
53
54 fn get_columns(block: &Value) -> ParseResult<&Vec<Value>> {
56 block
57 .get("columns")
58 .and_then(|v| v.as_array())
59 .ok_or_else(|| "Missing 'columns' array".to_string())
60 }
61
62 fn get_data(block: &Value) -> ParseResult<&Vec<Value>> {
64 block
65 .get("data")
66 .and_then(|v| v.as_array())
67 .ok_or_else(|| "Missing 'data' array".to_string())
68 }
69
70 fn find_column_index(columns: &[Value], name: &str) -> Option<usize> {
72 columns.iter().position(|col| col.as_str() == Some(name))
73 }
74
75 fn get_value<'a>(row: &'a Value, columns: &[Value], column: &str) -> Option<&'a Value> {
77 let row_array = row.as_array()?;
78 let index = Self::find_column_index(columns, column)?;
79 row_array.get(index)
80 }
81
82 fn parse_f64(value: &Value) -> Option<f64> {
84 value
85 .as_f64()
86 .or_else(|| value.as_str().and_then(|s| s.parse().ok()))
87 }
88
89 fn parse_timestamp(datetime_str: &str) -> Option<i64> {
99 let msk = FixedOffset::east_opt(3 * 3600)?;
100
101 if let Ok(ndt) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S") {
103 let local = msk.from_local_datetime(&ndt).single()?;
105 return Some(local.with_timezone(&Utc).timestamp_millis());
106 }
107 if let Ok(nd) = NaiveDate::parse_from_str(datetime_str, "%Y-%m-%d") {
109 let ndt = nd.and_hms_opt(0, 0, 0)?;
110 let local = msk.from_local_datetime(&ndt).single()?;
111 return Some(local.with_timezone(&Utc).timestamp_millis());
112 }
113 None
114 }
115
116 pub fn parse_price(response: &Value) -> ParseResult<f64> {
125 let block = Self::get_block(response, "marketdata")?;
126 let columns = Self::get_columns(block)?;
127 let data = Self::get_data(block)?;
128
129 let first_row = data.first().ok_or("Empty data array")?;
130 let last_value = Self::get_value(first_row, columns, "LAST")
131 .ok_or("Missing 'LAST' column")?;
132
133 Self::parse_f64(last_value)
134 .ok_or_else(|| "Invalid LAST price value".to_string())
135 }
136
137 pub fn parse_ticker(response: &Value, _symbol: &str) -> ParseResult<Ticker> {
147 let marketdata = Self::get_block(response, "marketdata")?;
148 let columns = Self::get_columns(marketdata)?;
149 let data = Self::get_data(marketdata)?;
150
151 let row = data.first().ok_or("Empty marketdata")?;
152
153 let last_price = Self::get_value(row, columns, "LAST")
155 .and_then(Self::parse_f64)
156 .ok_or("Missing LAST price")?;
157
158 let bid_price = Self::get_value(row, columns, "BID")
159 .and_then(Self::parse_f64);
160
161 let ask_price = Self::get_value(row, columns, "ASK")
162 .and_then(Self::parse_f64);
163
164 let high_24h = Self::get_value(row, columns, "HIGH")
165 .and_then(Self::parse_f64);
166
167 let low_24h = Self::get_value(row, columns, "LOW")
168 .and_then(Self::parse_f64);
169
170 let volume_24h = Self::get_value(row, columns, "VOLUME")
176 .and_then(Self::parse_f64);
177
178 let value = Self::get_value(row, columns, "VALUE")
179 .and_then(Self::parse_f64);
180
181 let change = Self::get_value(row, columns, "LASTCHANGE")
182 .and_then(Self::parse_f64);
183
184 let change_pct = Self::get_value(row, columns, "LASTCHANGEPRCNT")
185 .and_then(Self::parse_f64);
186
187 let timestamp = Self::get_value(row, columns, "SYSTIME")
189 .or_else(|| Self::get_value(row, columns, "UPDATETIME"))
190 .and_then(|v| v.as_str())
191 .and_then(Self::parse_timestamp)
192 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
193
194 Ok(Ticker {
195 last_price,
196 bid_price,
197 ask_price,
198 high_24h,
199 low_24h,
200 volume_24h,
201 quote_volume_24h: value,
202 price_change_24h: change,
203 price_change_percent_24h: change_pct,
204 timestamp,
205 })
206 }
207
208 pub fn parse_klines(response: &Value) -> ParseResult<Vec<Kline>> {
217 let block = Self::get_block(response, "candles")?;
218 let columns = Self::get_columns(block)?;
219 let data = Self::get_data(block)?;
220
221 data.iter()
222 .map(|row| {
223 let open = Self::get_value(row, columns, "open")
224 .and_then(Self::parse_f64)
225 .ok_or("Missing open")?;
226
227 let high = Self::get_value(row, columns, "high")
228 .and_then(Self::parse_f64)
229 .ok_or("Missing high")?;
230
231 let low = Self::get_value(row, columns, "low")
232 .and_then(Self::parse_f64)
233 .ok_or("Missing low")?;
234
235 let close = Self::get_value(row, columns, "close")
236 .and_then(Self::parse_f64)
237 .ok_or("Missing close")?;
238
239 let volume = Self::get_value(row, columns, "volume")
240 .and_then(Self::parse_f64)
241 .ok_or("Missing volume")?;
242
243 let quote_volume = Self::get_value(row, columns, "value")
244 .and_then(Self::parse_f64);
245
246 let open_time = Self::get_value(row, columns, "begin")
248 .and_then(|v| v.as_str())
249 .and_then(Self::parse_timestamp)
250 .ok_or("Missing begin timestamp")?;
251
252 let close_time = Self::get_value(row, columns, "end")
254 .and_then(|v| v.as_str())
255 .and_then(Self::parse_timestamp);
256
257 Ok(Kline {
258 open_time,
259 open,
260 high,
261 low,
262 close,
263 volume,
264 quote_volume,
265 close_time,
266 trades: None,
267 })
268 })
269 .collect()
270 }
271
272 pub fn parse_orderbook(response: &Value) -> ParseResult<OrderBook> {
281 let block = Self::get_block(response, "orderbook")?;
282 let columns = Self::get_columns(block)?;
283 let data = Self::get_data(block)?;
284
285 let mut bids = Vec::new();
288 let mut asks = Vec::new();
289
290 for row in data {
291 let side = Self::get_value(row, columns, "BUYSELL")
292 .and_then(|v| v.as_str());
293
294 let price = Self::get_value(row, columns, "PRICE")
295 .and_then(Self::parse_f64)
296 .ok_or("Missing price")?;
297
298 let quantity = Self::get_value(row, columns, "QUANTITY")
299 .and_then(Self::parse_f64)
300 .ok_or("Missing quantity")?;
301
302 match side {
303 Some("B") => bids.push(OrderBookLevel::new(price, quantity)),
304 Some("S") => asks.push(OrderBookLevel::new(price, quantity)),
305 _ => {}
306 }
307 }
308
309 bids.sort_by(|a, b| b.price.partial_cmp(&a.price).expect("f64 comparison should not return None"));
311 asks.sort_by(|a, b| a.price.partial_cmp(&b.price).expect("f64 comparison should not return None"));
312
313 let timestamp = chrono::Utc::now().timestamp_millis();
314
315 Ok(OrderBook {
316 bids,
317 asks,
318 timestamp,
319 sequence: None,
320 last_update_id: None,
321 first_update_id: None,
322 prev_update_id: None,
323 event_time: None,
324 transaction_time: None,
325 checksum: None,
326 })
327 }
328
329 pub fn parse_symbols(response: &Value) -> ParseResult<Vec<String>> {
338 let block = Self::get_block(response, "securities")?;
339 let columns = Self::get_columns(block)?;
340 let data = Self::get_data(block)?;
341
342 Ok(data
343 .iter()
344 .filter_map(|row| {
345 Self::get_value(row, columns, "SECID")
346 .and_then(|v| v.as_str())
347 .map(|s| s.to_string())
348 })
349 .collect())
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use serde_json::json;
357
358 #[test]
359 fn test_parse_price() {
360 let response = json!({
361 "marketdata": {
362 "columns": ["SECID", "LAST", "BID", "ASK"],
363 "data": [
364 ["SBER", 306.75, 306.74, 306.76]
365 ]
366 }
367 });
368
369 let price = MoexParser::parse_price(&response).unwrap();
370 assert_eq!(price, 306.75);
371 }
372
373 #[test]
374 fn test_parse_ticker() {
375 let response = json!({
376 "marketdata": {
377 "columns": ["SECID", "LAST", "BID", "ASK", "HIGH", "LOW", "VOLUME", "LASTCHANGE", "LASTCHANGEPRCNT", "SYSTIME"],
378 "data": [
379 ["SBER", 306.75, 306.74, 306.76, 307.35, 305.12, 4800000, -0.13, -0.04, "2026-01-26 19:00:01"]
380 ]
381 }
382 });
383
384 let ticker = MoexParser::parse_ticker(&response, "SBER").unwrap();
385 assert_eq!(ticker.last_price, 306.75);
386 assert_eq!(ticker.bid_price, Some(306.74));
387 assert_eq!(ticker.ask_price, Some(306.76));
388 }
389
390 #[test]
391 fn test_parse_symbols() {
392 let response = json!({
393 "securities": {
394 "columns": ["SECID", "SHORTNAME"],
395 "data": [
396 ["SBER", "Сбербанк"],
397 ["GAZP", "ГАЗПРОМ"],
398 ["LKOH", "ЛУКОЙЛ"]
399 ]
400 }
401 });
402
403 let symbols = MoexParser::parse_symbols(&response).unwrap();
404 assert_eq!(symbols, vec!["SBER", "GAZP", "LKOH"]);
405 }
406}