1use std::collections::{HashMap, HashSet};
11
12use regex::Regex;
13
14use crate::adjust::{AdjustInfo, adjust};
15use crate::config::{Config, PriceSettings, SYMBOL_RENAME_STRING, TRADING_SESSION_END_HOUR};
16use crate::error::{Result, ResultError};
17use crate::group::group;
18use crate::models::{ClosingPrice, Column, Instrument, Share};
19use crate::request::Requester;
20use crate::storage::Storage;
21use crate::util::{date_to_str, greg_to_shamsi, should_update, today};
22
23const MERGED_SYMBOL_CONTENT: &str = "merged";
24
25#[derive(Default)]
28struct PricesState {
29 stored_prices: HashMap<String, String>,
30 lastdevens: HashMap<String, String>,
31}
32
33#[derive(Clone)]
35pub struct Client {
36 config: Config,
37 storage: Storage,
38 requester: Requester,
39}
40
41#[derive(Debug, Clone)]
44pub enum Cell {
45 Text(String),
46 Number(f64),
47}
48
49#[derive(Debug, Clone, Default)]
50pub struct OhlcData {
51 pub date: Vec<String>,
52 pub open: Vec<f64>,
53 pub high: Vec<f64>,
54 pub low: Vec<f64>,
55 pub close: Vec<f64>,
56 pub volume: Vec<f64>,
57}
58
59#[derive(Debug, Clone, Default)]
61pub struct InstrumentPrices {
62 pub columns: HashMap<String, Vec<Cell>>,
64 pub ohlc_data: OhlcData,
65 pub adjust_info: Option<AdjustInfo>,
66 pub merged: bool,
68}
69
70#[derive(Debug, Default)]
72pub struct PricesResult {
73 pub data: Vec<Option<InstrumentPrices>>,
74 pub csv: Vec<Option<String>>,
75 pub error: Option<ResultError>,
76}
77
78impl Client {
79 pub fn new() -> Result<Self> {
80 let config = Config::default();
81 Ok(Client {
82 requester: Requester::new(config.api_url.clone()),
83 storage: Storage::new()?,
84 config,
85 })
86 }
87
88 pub fn with_parts(config: Config, storage: Storage) -> Self {
89 let requester = Requester::new(config.api_url.clone());
90 Client {
91 config,
92 storage,
93 requester,
94 }
95 }
96
97 pub fn config(&self) -> &Config {
98 &self.config
99 }
100
101 pub fn config_mut(&mut self) -> &mut Config {
102 &mut self.config
103 }
104
105 pub fn storage(&self) -> &Storage {
106 &self.storage
107 }
108
109 fn parse_instruments_by_symbol(&self) -> Result<HashMap<String, Instrument>> {
112 let rows = self.storage.get_item("tse.instruments")?;
113 let mut map = HashMap::new();
114 if rows.is_empty() {
115 return Ok(map);
116 }
117 for row in rows.split('\n') {
118 if row.is_empty() {
119 continue;
120 }
121 let ins = Instrument::parse(row)?;
122 map.insert(ins.symbol.clone(), ins);
123 }
124 Ok(map)
125 }
126
127 fn parse_shares_struct(&self) -> Result<Vec<Share>> {
128 let rows = self.storage.get_item("tse.shares")?;
129 let mut out = Vec::new();
130 if rows.is_empty() {
131 return Ok(out);
132 }
133 for row in rows.split('\n') {
134 if row.is_empty() {
135 continue;
136 }
137 out.push(Share::parse(row)?);
138 }
139 Ok(out)
140 }
141
142 fn parse_shares_raw(&self) -> Result<Vec<String>> {
143 let rows = self.storage.get_item("tse.shares")?;
144 if rows.is_empty() {
145 return Ok(vec![]);
146 }
147 Ok(rows.split('\n').map(|s| s.to_string()).collect())
148 }
149
150 async fn get_last_possible_devens(
152 &self,
153 ) -> Result<std::result::Result<(String, String), ResultError>> {
154 let mut no = String::new();
155 let mut id = String::new();
156
157 let stored = self.storage.get_item("tse.lastPossibleDevens")?;
158 if !stored.is_empty() {
159 let mut it = stored.split(',');
160 no = it.next().unwrap_or("").to_string();
161 id = it.next().unwrap_or("").to_string();
162 }
163
164 let today_str = date_to_str(today());
165 let last_update = self.storage.get_item("tse.lastLPDUpdate")?;
166 let today_num: i64 = today_str.parse().unwrap_or(0);
167 let last_update_num: i64 = last_update.parse().unwrap_or(0);
168 if today_num <= last_update_num {
169 return Ok(Ok((no, id)));
170 }
171
172 if stored.is_empty() || should_update(&today_str, &no) || should_update(&today_str, &id) {
173 match self.requester.last_possible_deven().await {
174 Ok(res) => {
175 let re = Regex::new(r"^\d{8};\d{8}$").unwrap();
176 if !re.is_match(res.trim()) {
177 return Ok(Err(ResultError::Request {
178 title: "Invalid server response: LastPossibleDeven".into(),
179 detail: String::new(),
180 }));
181 }
182 let parts: Vec<&str> = res.trim().split(';').collect();
183 self.storage
184 .set_item("tse.lastPossibleDevens", &parts.join(","))?;
185 self.storage.set_item("tse.lastLPDUpdate", &today_str)?;
186 no = parts[0].to_string();
187 id = parts[1].to_string();
188 }
189 Err(e) => {
190 return Ok(Err(ResultError::Request {
191 title: "Failed request: LastPossibleDeven".into(),
192 detail: e.to_string(),
193 }));
194 }
195 }
196 }
197
198 Ok(Ok((no, id)))
199 }
200
201 pub async fn update_instruments(&self) -> Result<Option<ResultError>> {
203 let last_update: i64 = self
204 .storage
205 .get_item("tse.lastInstrumentUpdate")?
206 .trim()
207 .parse()
208 .unwrap_or(0);
209 let now =
210 time::OffsetDateTime::now_local().unwrap_or_else(|_| time::OffsetDateTime::now_utc());
211 let today_deven: i64 = date_to_str(now.date()).parse().unwrap_or(0);
212 if last_update != 0
213 && (today_deven <= last_update || now.hour() <= TRADING_SESSION_END_HOUR)
214 {
215 return Ok(None);
216 }
217
218 let mut current_shares: Option<Vec<String>> = None;
219 let last_id: i64 = if last_update == 0 {
220 0
221 } else {
222 let cs = self.parse_shares_raw()?;
223 let max = cs
224 .iter()
225 .filter_map(|i| i.split(',').next())
226 .filter_map(|s| s.parse::<i64>().ok())
227 .max()
228 .unwrap_or(0);
229 current_shares = Some(cs);
230 max
231 };
232
233 let res = match self
234 .requester
235 .instrument_and_share(&today_deven.to_string(), last_id)
236 .await
237 {
238 Ok(r) => r,
239 Err(e) => {
240 return Ok(Some(ResultError::Request {
241 title: "Failed request: InstrumentAndShare".into(),
242 detail: e.to_string(),
243 }));
244 }
245 };
246 let shares_part = res.split('@').nth(1).unwrap_or("").to_string();
247
248 let instruments_raw = match self.requester.instrument("0").await {
249 Ok(r) => r,
250 Err(e) => {
251 return Ok(Some(ResultError::Request {
252 title: "Failed request: Instrument".into(),
253 detail: e.to_string(),
254 }));
255 }
256 };
257
258 let mut wrote_instruments = false;
259 if !instruments_raw.is_empty() && instruments_raw != "*" {
260 let instruments = Self::dedupe_and_rename(&instruments_raw);
261 self.storage.set_item("tse.instruments", &instruments)?;
262 wrote_instruments = true;
263 }
264
265 let mut wrote_shares = false;
266 if !shares_part.is_empty() {
267 let shares_str = if let Some(cs) = current_shares.filter(|c| !c.is_empty()) {
268 let mut all = cs;
269 all.extend(shares_part.split(';').map(|s| s.to_string()));
270 all.join("\n")
271 } else {
272 shares_part.replace(';', "\n")
273 };
274 self.storage.set_item("tse.shares", &shares_str)?;
275 wrote_shares = true;
276 }
277
278 if wrote_instruments || wrote_shares {
279 self.storage
280 .set_item("tse.lastInstrumentUpdate", &today_deven.to_string())?;
281 }
282
283 Ok(None)
284 }
285
286 fn dedupe_and_rename(instruments_raw: &str) -> String {
288 let mut rows: Vec<Vec<String>> = instruments_raw
289 .split(';')
290 .map(|i| i.split(',').map(|s| s.to_string()).collect())
291 .collect();
292
293 let cleaned: Vec<String> = rows
295 .iter()
296 .map(|r| crate::util::clean_fa(&r[5]).trim().to_string())
297 .collect();
298
299 let mut seen = HashSet::new();
301 let mut dup_syms: Vec<String> = Vec::new();
302 let mut counts: HashMap<&String, usize> = HashMap::new();
303 for s in &cleaned {
304 *counts.entry(s).or_insert(0) += 1;
305 }
306 for s in &cleaned {
307 if counts[s] > 1 && seen.insert(s.clone()) {
308 dup_syms.push(s.clone());
309 }
310 }
311
312 let code_idx: HashMap<String, usize> = rows
313 .iter()
314 .enumerate()
315 .map(|(j, r)| (r[0].clone(), j))
316 .collect();
317
318 for dsym in dup_syms {
319 let mut group: Vec<usize> = cleaned
321 .iter()
322 .enumerate()
323 .filter(|(_, c)| **c == dsym)
324 .map(|(i, _)| i)
325 .collect();
326 group.sort_by(|&a, &b| {
328 let da: i64 = rows[a][8].parse().unwrap_or(0);
329 let db: i64 = rows[b][8].parse().unwrap_or(0);
330 db.cmp(&da)
331 });
332
333 for (j, &ridx) in group.iter().enumerate() {
334 let orig = rows[ridx][5].clone();
335 if j > 0 {
336 let postfix = format!("{}{}", SYMBOL_RENAME_STRING, j + 1);
337 if rows[ridx].len() == 18 {
339 rows[ridx].push(orig.clone());
340 } else {
341 rows[ridx][18] = orig.clone();
342 }
343 rows[ridx][5] = format!("{}{}", orig.trim(), postfix);
344 } else {
345 rows[ridx][5] = orig;
346 }
347 }
348 }
349
350 let _ = code_idx; rows.iter()
353 .map(|r| r.join(","))
354 .collect::<Vec<_>>()
355 .join("\n")
356 }
357
358 async fn update_prices(
362 &self,
363 state: &mut PricesState,
364 selection: &[Option<Instrument>],
365 should_cache: bool,
366 ) -> Result<std::result::Result<(Vec<String>, Vec<String>), ResultError>> {
367 let raw = self.storage.get_item("tse.inscode_lastdeven")?;
369 let mut inscodes: HashSet<String> = HashSet::new();
370 if !raw.is_empty() {
371 for line in raw.split('\n') {
372 let mut it = line.split(',');
373 if let (Some(k), Some(v)) = (it.next(), it.next()) {
374 state.lastdevens.insert(k.to_string(), v.to_string());
375 inscodes.insert(k.to_string());
376 }
377 }
378 }
379
380 let last_possible = match self.get_last_possible_devens().await? {
381 Ok(v) => v,
382 Err(e) => return Ok(Err(e)),
383 };
384 let (lpd_no, lpd_id) = last_possible;
385 let first_possible_deven = "20010321";
386
387 let mut to_update: Vec<(String, String, i32)> = Vec::new();
388 for instrument in selection.iter().flatten() {
389 let inscode = &instrument.ins_code;
390 let market = &instrument.ymar_nsc;
391 let is_not_normal = if market == "NO" { 0 } else { 1 };
392
393 if !inscodes.contains(inscode) {
394 to_update.push((
395 inscode.clone(),
396 first_possible_deven.to_string(),
397 is_not_normal,
398 ));
399 } else {
400 let lastdeven = state.lastdevens.get(inscode).cloned().unwrap_or_default();
401 let last_possible_deven = if market != "NO" { &lpd_id } else { &lpd_no };
402 if lastdeven.is_empty() {
403 continue; }
405 if should_update(&lastdeven, last_possible_deven) {
406 to_update.push((inscode.clone(), lastdeven, is_not_normal));
407 }
408 }
409 }
410
411 let selins: HashSet<String> = selection
413 .iter()
414 .flatten()
415 .map(|i| i.ins_code.clone())
416 .collect();
417 let stored_has: HashSet<String> = state.stored_prices.keys().cloned().collect();
418 if stored_has.is_empty() || selins.iter().any(|i| !stored_has.contains(i)) {
419 self.storage.get_items(&selins, &mut state.stored_prices)?;
420 }
421
422 if to_update.is_empty() {
423 return Ok(Ok((vec![], vec![])));
424 }
425
426 let (succs, fails) = self
427 .run_prices_update(state, &to_update, should_cache, &lpd_no)
428 .await?;
429
430 if !succs.is_empty() && should_cache {
431 let str = state
432 .lastdevens
433 .iter()
434 .map(|(k, v)| format!("{k},{v}"))
435 .collect::<Vec<_>>()
436 .join("\n");
437 self.storage.set_item("tse.inscode_lastdeven", &str)?;
438 }
439
440 Ok(Ok((succs, fails)))
441 }
442
443 async fn run_prices_update(
447 &self,
448 state: &mut PricesState,
449 to_update: &[(String, String, i32)],
450 should_cache: bool,
451 last_possible_deven: &str,
452 ) -> Result<(Vec<String>, Vec<String>)> {
453 let chunk_size = self.config.prices_update_chunk;
454 let chunk_delay = self.config.prices_update_chunk_delay;
455 let retry_count = self.config.prices_update_retry_count;
456 let retry_delay = self.config.prices_update_retry_delay;
457
458 let mut succs: Vec<String> = Vec::new();
459 let mut fails: HashSet<String> = HashSet::new();
460
461 let mut chunks: Vec<Vec<(String, String, i32)>> =
463 to_update.chunks(chunk_size).map(|c| c.to_vec()).collect();
464
465 let mut retries = 0u32;
466 loop {
467 let mut retry_chunks: Vec<Vec<(String, String, i32)>> = Vec::new();
468
469 for (idx, chunk) in chunks.iter().enumerate() {
470 if idx > 0 && chunk_delay > 0 {
471 tokio::time::sleep(std::time::Duration::from_millis(chunk_delay)).await;
472 }
473 let ins_codes = chunk
474 .iter()
475 .map(|(c, d, m)| format!("{c},{d},{m}"))
476 .collect::<Vec<_>>()
477 .join(";");
478
479 match self.requester.closing_prices(&ins_codes).await {
480 Ok(resp) if is_valid_prices_response(&resp) => {
481 let parts: Vec<&str> = resp.split('@').collect();
482 for (i, item) in chunk.iter().enumerate() {
483 let inscode = &item.0;
484 let newdata = parts.get(i).copied().unwrap_or("").replace(';', "\n");
485 succs.push(inscode.clone());
486 fails.remove(inscode);
487
488 if !newdata.is_empty() {
489 let data = match state.stored_prices.get(inscode) {
490 Some(old) => format!("{old}\n{newdata}"),
491 None => newdata.clone(),
492 };
493 if let Some(last_row) = newdata.split('\n').next_back() {
495 if let Some(dv) = last_row.split(',').nth(1) {
496 state.lastdevens.insert(inscode.clone(), dv.to_string());
497 }
498 }
499 if should_cache {
500 self.storage.set_item_async(
501 &format!("tse.prices.{inscode}"),
502 &data,
503 false,
504 )?;
505 }
506 state.stored_prices.insert(inscode.clone(), data);
507 } else {
508 state
509 .lastdevens
510 .insert(inscode.clone(), last_possible_deven.to_string());
511 }
512 }
513 }
514 _ => {
515 for item in chunk {
516 fails.insert(item.0.clone());
517 }
518 retry_chunks.push(chunk.clone());
519 }
520 }
521 }
522
523 if retry_chunks.is_empty() || retries >= retry_count {
524 break;
525 }
526 retries += 1;
527 if retry_delay > 0 {
528 tokio::time::sleep(std::time::Duration::from_millis(retry_delay)).await;
529 }
530 for ch in &retry_chunks {
532 for item in ch {
533 fails.remove(&item.0);
534 }
535 }
536 chunks = retry_chunks;
537 }
538
539 Ok((succs, fails.into_iter().collect()))
540 }
541
542 pub async fn get_prices(
546 &self,
547 symbols: &[String],
548 settings: &PriceSettings,
549 ) -> Result<PricesResult> {
550 let mut result = PricesResult::default();
551 if symbols.is_empty() {
552 return Ok(result);
553 }
554
555 if let Some(err) = self.update_instruments().await? {
556 result.error = Some(err);
557 return Ok(result);
558 }
559
560 let instruments = self.parse_instruments_by_symbol()?;
561 let mut selection: Vec<Option<Instrument>> = symbols
563 .iter()
564 .map(|s| instruments.get(s).cloned())
565 .collect();
566 let not_founds: Vec<String> = symbols
567 .iter()
568 .zip(&selection)
569 .filter(|(_, sel)| sel.is_none())
570 .map(|(s, _)| s.clone())
571 .collect();
572 if !not_founds.is_empty() {
573 result.error = Some(ResultError::IncorrectSymbol {
574 symbols: not_founds,
575 });
576 return Ok(result);
577 }
578
579 let merge = settings.merge_similar_symbols;
581 let mut merges: HashMap<String, Vec<MergeItem>> = HashMap::new();
582 let mut extras_index: isize = -1;
583
584 if merge {
585 let re = Regex::new(&format!("{}(\\d+)", regex::escape(SYMBOL_RENAME_STRING))).unwrap();
586 let roots: HashSet<String> = instruments
587 .values()
588 .filter_map(|i| i.symbol_original.clone())
589 .collect();
590 for r in &roots {
591 merges.insert(r.clone(), Vec::new());
592 }
593 for i in instruments.values() {
594 let renamed_or_root = i
595 .symbol_original
596 .clone()
597 .unwrap_or_else(|| i.symbol.clone());
598 if let Some(v) = merges.get_mut(&renamed_or_root) {
599 let order = if let Some(_orig) = &i.symbol_original {
600 re.captures(&i.symbol)
601 .and_then(|c| c.get(1))
602 .and_then(|m| m.as_str().parse::<i64>().ok())
603 .unwrap_or(1)
604 } else {
605 1
606 };
607 v.push(MergeItem {
608 sym: i.symbol.clone(),
609 code: i.ins_code.clone(),
610 order,
611 });
612 }
613 }
614 for v in merges.values_mut() {
615 v.sort_by_key(|m| m.order);
616 }
617
618 let selsyms: HashSet<String> = selection
619 .iter()
620 .flatten()
621 .map(|i| i.symbol.clone())
622 .collect();
623 let mut extras: Vec<Instrument> = Vec::new();
624 for ins in selection.iter().flatten() {
625 if let Some(items) = merges.get(&ins.symbol) {
626 for leaf in items.iter().skip(1) {
627 if !selsyms.contains(&leaf.sym) {
628 if let Some(li) = instruments.get(&leaf.sym) {
629 extras.push(li.clone());
630 }
631 }
632 }
633 }
634 }
635 if !extras.is_empty() {
636 extras_index = selection.len() as isize;
637 selection.extend(extras.into_iter().map(Some));
638 }
639 }
640
641 let mut state = PricesState::default();
643 let update_result = self
644 .update_prices(&mut state, &selection, settings.cache)
645 .await?;
646 let (succs, fails) = match update_result {
647 Ok(v) => v,
648 Err(e) => {
649 result.error = Some(e);
650 return Ok(result);
651 }
652 };
653
654 if !fails.is_empty() {
655 let syms: HashMap<String, String> = selection
656 .iter()
657 .flatten()
658 .map(|i| (i.ins_code.clone(), i.symbol.clone()))
659 .collect();
660 let fail_set: HashSet<String> = fails.iter().cloned().collect();
661 result.error = Some(ResultError::IncompletePriceUpdate {
662 fails: fails.iter().filter_map(|k| syms.get(k).cloned()).collect(),
663 succs: succs.iter().filter_map(|k| syms.get(k).cloned()).collect(),
664 });
665 for sel in selection.iter_mut() {
666 if let Some(ins) = sel {
667 if fail_set.contains(&ins.ins_code) {
668 *sel = None;
669 }
670 }
671 }
672 }
673
674 if merge && extras_index > -1 {
675 selection.truncate(extras_index as usize);
676 }
677
678 let columns: Vec<Column> = settings
680 .columns
681 .iter()
682 .map(|&i| Column::new(i, None))
683 .collect::<Result<_>>()?;
684
685 let all_shares = self.parse_shares_struct()?;
686
687 let mut stored_prices_merged: HashMap<String, String> = HashMap::new();
689 if merge {
690 self.build_merged_prices(&state, &merges, &mut stored_prices_merged)?;
691 }
692
693 let textcols: HashSet<&str> = ["companycode", "namelatin", "symbol", "name"]
695 .into_iter()
696 .collect();
697
698 if settings.csv {
699 let headers = if settings.csv_headers {
700 columns
701 .iter()
702 .map(|c| c.header.clone())
703 .collect::<Vec<_>>()
704 .join(",")
705 + "\n"
706 } else {
707 String::new()
708 };
709 for instrument in &selection {
710 let Some(instrument) = instrument else {
711 result.csv.push(None);
712 continue;
713 };
714 let insdata = self.get_instrument_data(
715 instrument,
716 &state,
717 &merges,
718 &stored_prices_merged,
719 &all_shares,
720 settings,
721 )?;
722 let Some(insdata) = insdata else {
723 result.csv.push(Some(headers.clone()));
724 continue;
725 };
726 if insdata.merged {
727 result.csv.push(Some(MERGED_SYMBOL_CONTENT.to_string()));
728 continue;
729 }
730 if settings.get_adjust_info_only {
731 result.csv.push(Some(String::new()));
732 continue;
733 }
734 let body = insdata
735 .prices
736 .iter()
737 .map(|p| {
738 columns
739 .iter()
740 .map(|c| get_cell(c.name, instrument, p))
741 .collect::<Vec<_>>()
742 .join(&settings.csv_delimiter)
743 })
744 .collect::<Vec<_>>()
745 .join("\n");
746 result.csv.push(Some(format!("{headers}{body}")));
747 }
748 } else {
749 for instrument in &selection {
750 let Some(instrument) = instrument else {
751 result.data.push(None);
752 continue;
753 };
754 let mut out = InstrumentPrices {
755 columns: columns
756 .iter()
757 .map(|c| (c.header.clone(), Vec::new()))
758 .collect(),
759 ..Default::default()
760 };
761 let insdata = self.get_instrument_data(
762 instrument,
763 &state,
764 &merges,
765 &stored_prices_merged,
766 &all_shares,
767 settings,
768 )?;
769 let Some(insdata) = insdata else {
770 result.data.push(Some(out));
771 continue;
772 };
773 if insdata.merged {
774 out.merged = true;
775 result.data.push(Some(out));
776 continue;
777 }
778 out.adjust_info = insdata.adjust_info.clone();
779 if settings.get_adjust_info_only {
780 result.data.push(Some(out));
781 continue;
782 }
783 for p in &insdata.prices {
784 for c in &columns {
785 let cell = get_cell(c.name, instrument, p);
786 let col = out.columns.get_mut(&c.header).unwrap();
787 if textcols.contains(c.name) {
788 col.push(Cell::Text(cell));
789 } else {
790 col.push(Cell::Number(cell.parse::<f64>().unwrap_or(f64::NAN)));
791 }
792 }
793 }
794 insdata.prices.iter().for_each(|item| {
796 out.ohlc_data.date.push(item.deven.clone());
797 out.ohlc_data
798 .open
799 .push(item.price_first.parse::<f64>().unwrap_or(f64::NAN));
800 out.ohlc_data
801 .high
802 .push(item.price_max.parse::<f64>().unwrap_or(f64::NAN));
803 out.ohlc_data
804 .low
805 .push(item.price_min.parse::<f64>().unwrap_or(f64::NAN));
806 out.ohlc_data
807 .close
808 .push(item.pdr_cot_val.parse::<f64>().unwrap_or(f64::NAN));
809 out.ohlc_data
810 .volume
811 .push(item.qtot_tran5j.parse::<f64>().unwrap_or(f64::NAN));
812 });
813 result.data.push(Some(out));
814 }
815 }
816
817 Ok(result)
818 }
819
820 fn build_merged_prices(
821 &self,
822 state: &PricesState,
823 merges: &HashMap<String, Vec<MergeItem>>,
824 out: &mut HashMap<String, String>,
825 ) -> Result<()> {
826 for items in merges.values() {
827 let mut day_bounds: Vec<(i64, i64)> = Vec::new();
829 let codes: Vec<String> = items.iter().rev().map(|i| i.code.clone()).collect();
830 let latest_code = match codes.last() {
831 Some(c) => c.clone(),
832 None => continue,
833 };
834 for it in items.iter().rev() {
835 if let Some(data) = state.stored_prices.get(&it.code).filter(|d| !d.is_empty()) {
836 let rows: Vec<&str> = data.split('\n').collect();
837 let first = rows
838 .first()
839 .and_then(|r| r.split(',').nth(1))
840 .and_then(|s| s.parse::<i64>().ok())
841 .unwrap_or(0);
842 let last = rows
843 .last()
844 .and_then(|r| r.split(',').nth(1))
845 .and_then(|s| s.parse::<i64>().ok())
846 .unwrap_or(0);
847 day_bounds.push((first, last));
848 } else {
849 day_bounds.push((0, 0));
850 }
851 }
852
853 let flat: Vec<i64> = day_bounds.iter().flat_map(|(a, b)| [*a, *b]).collect();
855 let overlap = flat
856 .iter()
857 .enumerate()
858 .any(|(i, v)| i > 0 && *v < flat[i - 1]);
859
860 if overlap {
861 let fixed = self.merge_overlapping(state, &codes)?;
862 out.insert(latest_code, fixed);
863 } else {
864 let merged = codes
865 .iter()
866 .filter_map(|c| state.stored_prices.get(c))
867 .filter(|d| !d.is_empty())
868 .cloned()
869 .collect::<Vec<_>>()
870 .join("\n");
871 out.insert(latest_code, merged);
872 }
873 }
874 Ok(())
875 }
876
877 fn merge_overlapping(&self, state: &PricesState, codes: &[String]) -> Result<String> {
880 let mut a: Vec<ClosingPrice> = Vec::new();
881 for code in codes {
882 if let Some(data) = state.stored_prices.get(code).filter(|d| !d.is_empty()) {
883 for row in data.split('\n') {
884 a.push(ClosingPrice::parse(row)?);
885 }
886 }
887 }
888 if a.is_empty() {
889 return Ok(String::new());
890 }
891
892 let cp_eq = |x: &ClosingPrice, y: &ClosingPrice| x == y;
893 let sum = |xs: &[i64]| xs.iter().sum::<i64>();
894 let ins = |s: &str| s.parse::<i64>().unwrap_or(0);
895
896 let mut m: HashMap<String, ClosingPrice> = HashMap::new();
897 m.insert(a[0].deven.clone(), a[0].clone());
898
899 let len = a.len();
900 if len >= 2 {
901 for i in 1..len - 1 {
902 let prev = a[i - 1].clone();
903 let curr = a[i].clone();
904 let day = curr.deven.clone();
905 let mut select = curr.clone();
906 if let Some(existing) = m.get(&day).cloned() {
907 select = resolve_conflict(&existing, &curr, &prev, ins, sum);
908 }
909 m.insert(day, select);
910
911 let has_adj =
912 curr.ins_code == prev.ins_code && curr.price_yesterday != prev.pclosing;
913 if has_adj {
914 let yday = prev.deven.clone();
915 let mut select = prev.clone();
916 if let Some(existing) = m.get(&yday).cloned() {
917 if cp_eq(&existing, &prev) {
918 continue;
919 }
920 if existing.pclosing != prev.pclosing {
921 let trades = sum(&[ins(&existing.ztot_tran), ins(&prev.ztot_tran)]);
922 let mut row = prev.clone();
923 row.ztot_tran = trades.to_string();
924 select = row;
925 } else {
926 select = resolve_conflict(&existing, &prev, &curr, ins, sum);
927 }
928 }
929 m.insert(yday, select);
930 }
931 }
932 }
933
934 let mut fixed_rows: Vec<ClosingPrice> = m.into_values().collect();
935 fixed_rows.sort_by_key(|r| r.deven.parse::<i64>().unwrap_or(0));
936 Ok(fixed_rows
937 .iter()
938 .map(|r| r.to_csv_row())
939 .collect::<Vec<_>>()
940 .join("\n"))
941 }
942
943 fn get_instrument_data(
944 &self,
945 instrument: &Instrument,
946 state: &PricesState,
947 merges: &HashMap<String, Vec<MergeItem>>,
948 merged: &HashMap<String, String>,
949 all_shares: &[Share],
950 settings: &PriceSettings,
951 ) -> Result<Option<InstrumentDataOut>> {
952 let inscode = &instrument.ins_code;
953 let sym = &instrument.symbol;
954
955 let (prices_raw, inscodes): (Option<String>, HashSet<String>) =
956 if instrument.symbol_original.is_some() {
957 if settings.merge_similar_symbols {
958 return Ok(Some(InstrumentDataOut {
959 prices: vec![],
960 adjust_info: None,
961 merged: true,
962 }));
963 }
964 (
965 state.stored_prices.get(inscode).cloned(),
966 HashSet::from([inscode.clone()]),
967 )
968 } else {
969 let is_root = merges.contains_key(sym);
970 if is_root {
971 let codes: HashSet<String> = merges
972 .get(sym)
973 .map(|v| v.iter().map(|m| m.code.clone()).collect())
974 .unwrap_or_default();
975 (merged.get(inscode).cloned(), codes)
976 } else {
977 (
978 state.stored_prices.get(inscode).cloned(),
979 HashSet::from([inscode.clone()]),
980 )
981 }
982 };
983
984 let Some(prices_raw) = prices_raw.filter(|p| !p.is_empty()) else {
985 return Ok(None);
986 };
987
988 let mut prices: Vec<ClosingPrice> = prices_raw
989 .split('\n')
990 .map(ClosingPrice::parse)
991 .collect::<Result<_>>()?;
992
993 let mut adjust_info: Option<AdjustInfo> = None;
994 let should_get_info = settings.get_adjust_info || settings.get_adjust_info_only;
995 if settings.adjust_prices > 0 || should_get_info {
996 let related: HashMap<String, Share> = all_shares
997 .iter()
998 .filter(|s| inscodes.contains(&s.ins_code))
999 .map(|s| (s.deven.clone(), s.clone()))
1000 .collect();
1001 let res = adjust(
1002 settings.adjust_prices,
1003 &prices,
1004 &related,
1005 settings.get_adjust_info,
1006 settings.get_adjust_info_only,
1007 );
1008 if settings.adjust_prices > 0 {
1009 prices = res.prices.unwrap_or_default();
1010 }
1011 if should_get_info {
1012 adjust_info = res.info;
1013 }
1014 }
1015
1016 if !settings.days_without_trade {
1017 prices.retain(|p| p.ztot_tran.parse::<i64>().unwrap_or(0) > 0);
1018 }
1019
1020 let start: i64 = settings.start_date.parse().unwrap_or(0);
1022 if !settings.end_date.is_empty() {
1023 let end: i64 = settings.end_date.parse().unwrap_or(i64::MAX);
1024 if start > 0 && end < i64::MAX && start > end {
1025 prices.clear();
1027 return Ok(Some(InstrumentDataOut {
1028 prices,
1029 adjust_info,
1030 merged: false,
1031 }));
1032 }
1033 }
1034
1035 prices.retain(|p| p.deven.parse::<i64>().unwrap_or(0) >= start);
1036
1037 if !settings.end_date.is_empty() {
1039 let end: i64 = settings.end_date.parse().unwrap_or(i64::MAX);
1040 prices.retain(|p| p.deven.parse::<i64>().unwrap_or(0) <= end);
1041 }
1042
1043 let prices = group(&prices, settings.period);
1047
1048 Ok(Some(InstrumentDataOut {
1049 prices,
1050 adjust_info,
1051 merged: false,
1052 }))
1053 }
1054}
1055
1056struct InstrumentDataOut {
1057 prices: Vec<ClosingPrice>,
1058 adjust_info: Option<AdjustInfo>,
1059 merged: bool,
1060}
1061
1062#[derive(Debug, Clone)]
1063struct MergeItem {
1064 sym: String,
1065 code: String,
1066 order: i64,
1067}
1068
1069fn resolve_conflict(
1072 existing: &ClosingPrice,
1073 candidate: &ClosingPrice,
1074 reference: &ClosingPrice,
1075 ins: impl Fn(&str) -> i64,
1076 sum: impl Fn(&[i64]) -> i64,
1077) -> ClosingPrice {
1078 let ref_ins = ins(&reference.ins_code);
1080 let candidates = [existing, candidate];
1081 let mut ranked: Vec<(&ClosingPrice, i64, i64)> = candidates
1082 .iter()
1083 .map(|c| (*c, ins(&c.ztot_tran), (ins(&c.ins_code) - ref_ins).abs()))
1084 .collect();
1085 ranked.sort_by_key(|a| a.2);
1089 ranked.sort_by_key(|a| std::cmp::Reverse(a.1));
1090 let chosen = ranked[0].0;
1091 let trades = sum(&[ins(&existing.ztot_tran), ins(&candidate.ztot_tran)]);
1092 let mut row = chosen.clone();
1093 row.ztot_tran = trades.to_string();
1094 row
1095}
1096
1097fn get_cell(name: &str, instrument: &Instrument, cp: &ClosingPrice) -> String {
1099 match name {
1100 "date" => cp.deven.clone(),
1101 "dateshamsi" => greg_to_shamsi(&cp.deven),
1102 "open" => cp.price_first.clone(),
1103 "high" => cp.price_max.clone(),
1104 "low" => cp.price_min.clone(),
1105 "last" => cp.pdr_cot_val.clone(),
1106 "close" => cp.pclosing.clone(),
1107 "vol" => cp.qtot_tran5j.clone(),
1108 "count" => cp.ztot_tran.clone(),
1109 "value" => cp.qtot_cap.clone(),
1110 "yesterday" => cp.price_yesterday.clone(),
1111 "symbol" => instrument.symbol.clone(),
1112 "name" => instrument.name.clone(),
1113 "namelatin" => instrument.latin_name.clone(),
1114 "companycode" => instrument.company_code.clone(),
1115 _ => String::new(),
1116 }
1117}
1118
1119fn is_valid_prices_response(resp: &str) -> bool {
1121 if resp.is_empty() {
1122 return true;
1123 }
1124 resp.chars()
1125 .all(|c| c.is_ascii_digit() || matches!(c, '.' | ',' | ';' | '@' | '-'))
1126}