1use std::collections::{HashMap, HashSet};
8
9use crate::client::Client;
10use crate::config::IntradaySettings;
11use crate::error::{Error, Result, ResultError};
12use crate::request::intraday_url;
13use crate::storage::ItdValue;
14use crate::util::clean_fa;
15
16pub type SymbolIntraday = Vec<(String, Option<ItdValue>)>;
19
20#[derive(Debug, Default)]
22pub struct IntradayResult {
23 pub data: Vec<Option<SymbolIntraday>>,
25 pub error: Option<ResultError>,
26}
27
28fn parse_raw(separator: &str, text: &str) -> Result<serde_json::Value> {
30 let after = text
31 .split(separator)
32 .nth(1)
33 .ok_or_else(|| Error::InvalidData(format!("missing block: {separator}")))?;
34 let inner = after.split("];").next().unwrap_or("");
35 let json = format!("[{}]", inner.replace('\'', "\""));
36 Ok(serde_json::from_str(&json)?)
37}
38
39fn cell(v: &serde_json::Value) -> String {
40 match v {
41 serde_json::Value::String(s) => s.clone(),
42 serde_json::Value::Number(n) => n.to_string(),
43 serde_json::Value::Bool(b) => b.to_string(),
44 serde_json::Value::Null => String::new(),
45 other => other.to_string(),
46 }
47}
48
49fn extract_record(text: &str) -> Result<String> {
52 if text == "N/A" {
53 return Ok("N/A".to_string());
54 }
55 let closing = parse_raw("var ClosingPriceData=[", text)?;
56 let best_limit = parse_raw("var BestLimitData=[", text)?;
57 let intra_trade = parse_raw("var IntraTradeData=[", text)?;
58 let client_type = parse_raw("var ClientTypeData=[", text)?;
59 let inst_state = parse_raw("var InstrumentStateData=[", text)?;
60 let static_tresh = parse_raw("var StaticTreshholdData=[", text)?;
61 let inst_simple = parse_raw("var InstSimpleData=[", text)?;
62 let share_holder = parse_raw("var ShareHolderData=[", text)?;
63
64 let row_cols = |row: &serde_json::Value, idxs: &[usize]| -> String {
65 idxs.iter()
66 .map(|&i| row.get(i).map(cell).unwrap_or_default())
67 .collect::<Vec<_>>()
68 .join(",")
69 };
70
71 let price = arr(&closing)
73 .iter()
74 .map(|r| row_cols(r, &[12, 2, 3, 4, 6, 7, 8, 9, 10, 11]))
75 .collect::<Vec<_>>()
76 .join("\n");
77
78 let order = arr(&best_limit)
80 .iter()
81 .map(|r| row_cols(r, &[0, 1, 2, 3, 4, 5, 6, 7]))
82 .collect::<Vec<_>>()
83 .join("\n");
84
85 let mut trade_rows: Vec<Vec<String>> = arr(&intra_trade)
87 .iter()
88 .map(|r| {
89 let time_str = r.get(1).map(cell).unwrap_or_default();
90 let parts: Vec<&str> = time_str.split(':').collect();
91 let h: i64 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
92 let m: i64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
93 let s: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
94 let timeint = (h * 10000 + m * 100 + s).to_string();
95 vec![
97 timeint,
98 r.get(0).map(cell).unwrap_or_default(),
99 r.get(2).map(cell).unwrap_or_default(),
100 r.get(3).map(cell).unwrap_or_default(),
101 r.get(4).map(cell).unwrap_or_default(),
102 ]
103 })
104 .collect();
105 trade_rows.sort_by_key(|r| r[0].parse::<i64>().unwrap_or(0));
106 let trade = trade_rows
107 .iter()
108 .map(|r| r.join(","))
109 .collect::<Vec<_>>()
110 .join("\n");
111
112 let ct = arr(&client_type);
114 let client = [
115 4, 0, 12, 16, 8, 6, 2, 14, 18, 10, 5, 1, 13, 17, 9, 7, 3, 15, 19, 11, 20,
116 ]
117 .iter()
118 .map(|&i| ct.get(i).map(cell).unwrap_or_default())
119 .collect::<Vec<_>>()
120 .join(",");
121
122 let a = arr(&inst_state);
124 let b = arr(&static_tresh);
125 let state = a
126 .first()
127 .and_then(|r| r.get(2))
128 .map(cell)
129 .unwrap_or_default();
130 let (daymin, daymax) = if b.len() > 1 {
131 (
132 b[1].get(2).map(cell).unwrap_or_default(),
133 b[1].get(1).map(cell).unwrap_or_default(),
134 )
135 } else {
136 (String::new(), String::new())
137 };
138 let is = arr(&inst_simple);
139 let flow = is.get(4).map(cell).unwrap_or_default();
140 let basevol = is.get(9).map(cell).unwrap_or_default();
141 let misc = [basevol, flow, daymin, daymax, state].join(",");
142
143 let shareholder = arr(&share_holder)
145 .iter()
146 .filter(|r| r.get(4).map(|v| !cell(v).is_empty()).unwrap_or(false))
147 .map(|r| {
148 let arrow = match cell(&r[4]).as_str() {
149 "ArrowUp" => "+",
150 "ArrowDown" => "-",
151 _ => "",
152 }
153 .to_string();
154 let name = clean_fa(&cell(r.get(5).unwrap_or(&serde_json::Value::Null)));
155 [
157 r.get(2).map(cell).unwrap_or_default(),
158 r.get(3).map(cell).unwrap_or_default(),
159 arrow,
160 r.get(0).map(cell).unwrap_or_default(),
161 name,
162 ]
163 .join(",")
164 })
165 .collect::<Vec<_>>()
166 .join("\n");
167
168 let mut file = vec![price, order, trade, client, misc];
169 if !shareholder.is_empty() {
170 file.push(shareholder);
171 }
172 Ok(file.join("\n\n"))
173}
174
175fn arr(v: &serde_json::Value) -> Vec<serde_json::Value> {
176 v.as_array().cloned().unwrap_or_default()
177}
178
179impl Client {
180 pub async fn get_intraday(
182 &self,
183 symbols: &[String],
184 settings: &IntradaySettings,
185 ) -> Result<IntradayResult> {
186 let mut result = IntradayResult::default();
187 if symbols.is_empty() {
188 return Ok(result);
189 }
190
191 if let Some(err) = self.update_instruments().await? {
192 result.error = Some(err);
193 return Ok(result);
194 }
195
196 let instruments = self.get_instruments().await?;
197 let by_symbol: HashMap<String, &crate::models::Instrument> = instruments
199 .values()
200 .map(|i| (i.symbol.clone(), i))
201 .collect();
202
203 let selection: Vec<Option<crate::models::Instrument>> = symbols
204 .iter()
205 .map(|s| by_symbol.get(s).map(|i| (*i).clone()))
206 .collect();
207 let not_founds: Vec<String> = symbols
208 .iter()
209 .zip(&selection)
210 .filter(|(_, s)| s.is_none())
211 .map(|(s, _)| s.clone())
212 .collect();
213 if !not_founds.is_empty() {
214 result.error = Some(ResultError::IncorrectSymbol {
215 symbols: not_founds,
216 });
217 return Ok(result);
218 }
219
220 let selins: HashSet<String> = selection
221 .iter()
222 .flatten()
223 .map(|i| i.ins_code.clone())
224 .collect();
225
226 let stored_raw = self.storage().get_item_async("tse.inscode_devens", false)?;
230 let mut stored_inscode_devens: HashMap<String, Vec<i64>> = HashMap::new();
231 if !stored_raw.is_empty() {
232 for line in stored_raw.split('\n') {
233 let mut it = line.split(';');
234 if let (Some(i), Some(d)) = (it.next(), it.next()) {
235 let devens = d.split(',').filter_map(|x| x.parse::<i64>().ok()).collect();
236 stored_inscode_devens.insert(i.to_string(), devens);
237 }
238 }
239 }
240
241 let start_date: i64 = settings.start_date.parse().unwrap_or(0);
242 let end_date: i64 = settings.end_date.parse().unwrap_or(0);
243
244 let in_range = |i: i64| -> bool {
245 if end_date != 0 {
246 i >= start_date && i <= end_date
247 } else {
248 i >= start_date
249 }
250 };
251
252 let asked_inscode_devens: Vec<(String, Vec<i64>)> = selins
253 .iter()
254 .map(|inscode| {
255 let devens = stored_inscode_devens
256 .get(inscode)
257 .map(|all| all.iter().copied().filter(|d| in_range(*d)).collect())
258 .unwrap_or_default();
259 (inscode.clone(), devens)
260 })
261 .collect();
262
263 let mut stored = self.storage().itd_get_items(
265 &selins,
266 if settings.update_only {
267 settings.re_update_no_trades
268 } else {
269 true
270 },
271 )?;
272
273 let mut to_update: Vec<(String, Vec<i64>)> = Vec::new();
275 for (inscode, devens) in &asked_inscode_devens {
276 if devens.is_empty() {
277 continue;
278 }
279 match stored.get(inscode) {
280 None => to_update.push((inscode.clone(), devens.clone())),
281 Some(have) => {
282 let need: Vec<i64> = if settings.re_update_no_trades {
283 devens
284 .iter()
285 .copied()
286 .filter(|d| {
287 match have.get(&d.to_string()).and_then(|v| v.to_text()) {
288 None => true,
289 Some(text) => {
290 text.split("\n\n")
292 .nth(2)
293 .map(|t| t.is_empty())
294 .unwrap_or(true)
295 }
296 }
297 })
298 .collect()
299 } else {
300 devens
301 .iter()
302 .copied()
303 .filter(|d| !have.contains_key(&d.to_string()))
304 .collect()
305 };
306 if !need.is_empty() {
307 to_update.push((inscode.clone(), need));
308 }
309 }
310 }
311 }
312
313 let mut servers = settings.servers.clone();
314 if servers.is_empty() || servers.iter().any(|&s| s < 0 && s != -1) {
315 servers = self.config().intraday_update_servers.clone();
316 }
317
318 if !to_update.is_empty() {
319 let (succs, fails) = self
320 .run_intraday_update(&mut stored, &to_update, settings, &servers)
321 .await?;
322
323 if !fails.is_empty() {
324 let k: HashMap<String, String> = selection
325 .iter()
326 .flatten()
327 .map(|i| (i.ins_code.clone(), i.symbol.clone()))
328 .collect();
329 let group = |pairs: &[(String, String)]| -> HashMap<String, Vec<String>> {
330 let mut o: HashMap<String, Vec<String>> = HashMap::new();
331 for (i, d) in pairs {
332 let sym = k.get(i).cloned().unwrap_or_else(|| i.clone());
333 o.entry(sym).or_default().push(d.clone());
334 }
335 o
336 };
337 result.error = Some(ResultError::IncompleteIntradayUpdate {
338 fails: group(&fails),
339 succs: group(&succs),
340 });
341 }
342 }
343
344 if !settings.update_only {
345 result.data = asked_inscode_devens
346 .iter()
347 .map(|(inscode, devens)| {
348 stored.get(inscode).map(|instr| {
349 devens
350 .iter()
351 .map(|deven| {
352 let key = deven.to_string();
353 let val = instr.get(&key).cloned();
354 if settings.gzip {
355 (key, val)
356 } else {
357 let plain = val.and_then(|v| match &v {
358 ItdValue::Present => None,
359 ItdValue::Plain(_) => Some(v.clone()),
360 ItdValue::Gzipped(_) => v.to_text().map(ItdValue::Plain),
361 });
362 (key, plain)
363 }
364 })
365 .collect()
366 })
367 })
368 .collect();
369 }
370
371 Ok(result)
372 }
373
374 async fn run_intraday_update(
377 &self,
378 stored: &mut HashMap<String, HashMap<String, ItdValue>>,
379 to_update: &[(String, Vec<i64>)],
380 settings: &IntradaySettings,
381 servers: &[i32],
382 ) -> Result<(Vec<(String, String)>, Vec<(String, String)>)> {
383 let chunk_delay = settings.chunk_delay;
384 let retry_count = settings.retry_count;
385 let retry_delay = settings.retry_delay;
386 let should_cache = settings.cache;
387
388 let mut chunks: Vec<(usize, String, String)> = Vec::new();
390 for (inscode, devens) in to_update {
391 for d in devens {
392 chunks.push((0usize, inscode.clone(), d.to_string()));
393 }
394 }
395
396 let mut succs: Vec<(String, String)> = Vec::new();
397 let mut fails: HashSet<(String, String)> = HashSet::new();
398 let mut extracted_ins: HashMap<String, String> = HashMap::new();
400 let mut inslastdeven: HashMap<String, String> = HashMap::new();
402 for (inscode, devens) in to_update {
403 if let Some(last) = devens.last() {
404 inslastdeven.insert(inscode.clone(), last.to_string());
405 }
406 }
407
408 let mut retries = 0u32;
409 loop {
410 let mut retry_chunks: Vec<(usize, String, String)> = Vec::new();
411
412 for (i, (srv_idx, inscode, deven)) in chunks.iter().enumerate() {
413 if i > 0 && chunk_delay > 0 {
414 tokio::time::sleep(std::time::Duration::from_millis(chunk_delay)).await;
415 }
416 let server = servers[*srv_idx % servers.len()];
417 let url = intraday_url(server, inscode, deven);
418 let outcome = self.fetch_intraday(&url, settings.chunk_max_wait).await;
419
420 match outcome {
421 Some(text) => {
422 fails.remove(&(inscode.clone(), deven.clone()));
423 succs.push((inscode.clone(), deven.clone()));
424
425 let is_last = inslastdeven
427 .get(inscode)
428 .map(|s| s == deven)
429 .unwrap_or(false);
430 if is_last && text != "N/A" {
431 if let Some(after) = text.split("var InstSimpleData=").nth(1) {
432 let block = after.split(';').next().unwrap_or("");
433 let json = block.replace('\'', "\"");
434 if let Ok(serde_json::Value::Array(row)) =
435 serde_json::from_str::<serde_json::Value>(&json)
436 {
437 let mut parts = vec![inscode.clone()];
438 parts.extend(row.iter().map(cell));
439 extracted_ins.insert(inscode.clone(), parts.join(","));
440 }
441 }
442 }
443
444 match extract_record(&text) {
446 Ok(record) => {
447 let entry = stored.entry(inscode.clone()).or_default();
448 entry.insert(deven.clone(), ItdValue::Plain(record));
449 if should_cache {
450 let single: HashMap<String, ItdValue> = entry
451 .iter()
452 .filter(|(_, v)| !matches!(v, ItdValue::Present))
453 .map(|(k, v)| (k.clone(), v.clone()))
454 .collect();
455 self.storage().itd_set_item(inscode, &single)?;
456 }
457 }
458 Err(_) => {
459 fails.insert((inscode.clone(), deven.clone()));
461 retry_chunks.push((
462 next_server(servers, *srv_idx),
463 inscode.clone(),
464 deven.clone(),
465 ));
466 }
467 }
468 }
469 None => {
470 fails.insert((inscode.clone(), deven.clone()));
471 retry_chunks.push((
472 next_server(servers, *srv_idx),
473 inscode.clone(),
474 deven.clone(),
475 ));
476 }
477 }
478 }
479
480 if retry_chunks.is_empty() || retries >= retry_count {
481 break;
482 }
483 retries += 1;
484 if retry_delay > 0 {
485 tokio::time::sleep(std::time::Duration::from_millis(retry_delay)).await;
486 }
487 for (_, inscode, deven) in &retry_chunks {
488 fails.remove(&(inscode.clone(), deven.clone()));
489 }
490 chunks = retry_chunks;
491 }
492
493 if !extracted_ins.is_empty() {
495 let mut current = self.get_intraday_instruments_raw()?;
496 current.extend(extracted_ins);
497 let joined = current.values().cloned().collect::<Vec<_>>().join("\n");
498 self.storage()
499 .set_item("tse.instruments.intraday", &joined)?;
500 }
501
502 Ok((succs, fails.into_iter().collect()))
503 }
504
505 fn get_intraday_instruments_raw(&self) -> Result<HashMap<String, String>> {
506 let rows = self.storage().get_item("tse.instruments.intraday")?;
507 let mut map = HashMap::new();
508 if rows.is_empty() {
509 return Ok(map);
510 }
511 for row in rows.split('\n').filter(|r| !r.is_empty()) {
512 if let Some(code) = row.split(',').next() {
513 map.insert(code.to_string(), row.to_string());
514 }
515 }
516 Ok(map)
517 }
518
519 async fn fetch_intraday(&self, url: &str, max_wait: u64) -> Option<String> {
522 let client = reqwest::Client::builder()
523 .timeout(std::time::Duration::from_millis(max_wait.max(1)))
524 .build()
525 .ok()?;
526 let res = client.get(url).send().await.ok()?;
527 if res.status().as_u16() != 200 {
528 return None;
529 }
530 let text = res.text().await.ok()?;
531 if text.contains(
532 "Object moved to <a href=\"/GeneralError.aspx?aspxerrorpath=/Loader.aspx\">here</a>",
533 ) {
534 return Some("N/A".to_string());
535 }
536 let after = text.split("var StaticTreshholdData").nth(1)?;
538 let up_to_date = [
539 "ClosingPrice",
540 "IntraDayPrice",
541 "IntraTrade",
542 "ClientType",
543 "BestLimit",
544 ]
545 .iter()
546 .any(|name| {
547 after
548 .split(&format!("var {name}Data=["))
549 .nth(1)
550 .and_then(|s| s.split("];").next())
551 .map(|s| !s.is_empty())
552 .unwrap_or(false)
553 });
554 if up_to_date { Some(text) } else { None }
555 }
556}
557
558fn next_server(servers: &[i32], current_idx: usize) -> usize {
559 (current_idx + 1) % servers.len()
560}