1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::error::MetadataParseError;
6
7fn parse_holiday_token(token: &str) -> Option<i64> {
8 let token = token.trim();
9 if token.is_empty() {
10 return None;
11 }
12 if token.len() == 8 {
13 return token.parse::<i64>().ok();
14 }
15 token
16 .parse::<i64>()
17 .ok()
18 .map(|value| value.div_euclid(86_400_000))
19}
20
21fn detect_holiday_roots() -> Vec<PathBuf> {
22 let mut out = Vec::new();
23 if let Ok(path) = std::env::var("XTQUANT_HOLIDAY_DIR") {
24 out.push(PathBuf::from(path));
25 }
26 if let Ok(path) = std::env::var("XTQUANT_APPDATA_DIR") {
27 out.push(PathBuf::from(path));
28 }
29 if let Ok(path) = std::env::var("XTQUANT_DATA_DIR") {
30 out.push(PathBuf::from(path));
31 }
32 out
33}
34
35fn detect_weight_roots() -> Vec<PathBuf> {
36 let mut out = Vec::new();
37 if let Ok(path) = std::env::var("XTQUANT_WEIGHT_DIR") {
38 out.push(PathBuf::from(path));
39 }
40 if let Ok(path) = std::env::var("XTQUANT_APPDATA_DIR") {
41 out.push(PathBuf::from(path));
42 }
43 if let Ok(path) = std::env::var("XTQUANT_DATA_DIR") {
44 out.push(PathBuf::from(path));
45 }
46 out
47}
48
49fn detect_industry_roots() -> Vec<PathBuf> {
50 let mut out = Vec::new();
51 if let Ok(path) = std::env::var("XTQUANT_INDUSTRY_DIR") {
52 out.push(PathBuf::from(path));
53 }
54 if let Ok(path) = std::env::var("XTQUANT_APPDATA_DIR") {
55 out.push(PathBuf::from(path));
56 }
57 if let Ok(path) = std::env::var("XTQUANT_DATA_DIR") {
58 out.push(PathBuf::from(path));
59 }
60 out
61}
62
63fn detect_sector_roots() -> Vec<PathBuf> {
64 let mut out = Vec::new();
65 if let Ok(path) = std::env::var("XTQUANT_SECTOR_DIR") {
66 out.push(PathBuf::from(path));
67 }
68 if let Ok(path) = std::env::var("XTQUANT_APPDATA_DIR") {
69 out.push(PathBuf::from(path));
70 }
71 if let Ok(path) = std::env::var("XTQUANT_DATA_DIR") {
72 out.push(PathBuf::from(path));
73 }
74 out
75}
76
77fn resolve_holiday_csv() -> Result<PathBuf, MetadataParseError> {
78 resolve_holiday_csv_from_roots(&detect_holiday_roots())
79}
80
81fn resolve_holiday_dat() -> Result<PathBuf, MetadataParseError> {
82 resolve_holiday_dat_from_roots(&detect_holiday_roots())
83}
84
85fn resolve_sector_name_files() -> Vec<PathBuf> {
86 resolve_sector_name_files_from_roots(&detect_weight_roots())
87}
88
89fn resolve_sector_weight_file() -> Result<PathBuf, MetadataParseError> {
90 resolve_sector_weight_file_from_roots(&detect_weight_roots())
91}
92
93fn resolve_sectorlist_dat() -> Result<PathBuf, MetadataParseError> {
94 resolve_sectorlist_dat_from_roots(&detect_sector_roots())
95}
96
97fn resolve_industry_file() -> Result<PathBuf, MetadataParseError> {
98 resolve_industry_file_from_roots(&detect_industry_roots())
99}
100
101fn resolve_holiday_csv_from_roots(roots: &[PathBuf]) -> Result<PathBuf, MetadataParseError> {
102 for root in roots {
103 if !root.exists() {
104 continue;
105 }
106 for path in [
107 root.join("holiday.csv"),
108 root.join("holiday").join("holiday.csv"),
109 ] {
110 if path.exists() {
111 return Ok(path);
112 }
113 }
114 }
115 Err(MetadataParseError::Io(std::io::Error::new(
116 std::io::ErrorKind::NotFound,
117 "holiday.csv not found",
118 )))
119}
120
121fn resolve_holiday_dat_from_roots(roots: &[PathBuf]) -> Result<PathBuf, MetadataParseError> {
122 for root in roots {
123 if !root.exists() {
124 continue;
125 }
126 for path in [
127 root.join("holiday.dat"),
128 root.join("holiday").join("holiday.dat"),
129 ] {
130 if path.exists() {
131 return Ok(path);
132 }
133 }
134 }
135 Err(MetadataParseError::Io(std::io::Error::new(
136 std::io::ErrorKind::NotFound,
137 "holiday.dat not found",
138 )))
139}
140
141fn resolve_sector_name_files_from_roots(roots: &[PathBuf]) -> Vec<PathBuf> {
142 let mut out = Vec::new();
143 for root in roots {
144 if !root.exists() {
145 continue;
146 }
147 for path in [
148 root.join("systemSectorWeightData.txt"),
149 root.join("customSectorWeightData.txt"),
150 root.join("Weight").join("systemSectorWeightData.txt"),
151 root.join("Weight").join("customSectorWeightData.txt"),
152 ] {
153 if path.exists() {
154 out.push(path);
155 }
156 }
157 }
158 out
159}
160
161fn resolve_sector_weight_file_from_roots(roots: &[PathBuf]) -> Result<PathBuf, MetadataParseError> {
162 for root in roots {
163 if !root.exists() {
164 continue;
165 }
166 for path in [
167 root.join("sectorWeightData.txt"),
168 root.join("Weight").join("sectorWeightData.txt"),
169 ] {
170 if path.exists() {
171 return Ok(path);
172 }
173 }
174 }
175 Err(MetadataParseError::Io(std::io::Error::new(
176 std::io::ErrorKind::NotFound,
177 "sectorWeightData.txt not found",
178 )))
179}
180
181fn resolve_sectorlist_dat_from_roots(roots: &[PathBuf]) -> Result<PathBuf, MetadataParseError> {
182 for root in roots {
183 if !root.exists() {
184 continue;
185 }
186 for path in [
187 root.join("sectorlist.DAT"),
188 root.join("Sector").join("sectorlist.DAT"),
189 root.join("Sector").join("Temple").join("sectorlist.DAT"),
190 ] {
191 if path.exists() {
192 return Ok(path);
193 }
194 }
195 }
196 Err(MetadataParseError::Io(std::io::Error::new(
197 std::io::ErrorKind::NotFound,
198 "sectorlist.DAT not found",
199 )))
200}
201
202fn resolve_industry_file_from_roots(roots: &[PathBuf]) -> Result<PathBuf, MetadataParseError> {
203 for root in roots {
204 if !root.exists() {
205 continue;
206 }
207 for path in [
208 root.join("IndustryData.txt"),
209 root.join("Industry").join("IndustryData.txt"),
210 ] {
211 if path.exists() {
212 return Ok(path);
213 }
214 }
215 }
216 Err(MetadataParseError::Io(std::io::Error::new(
217 std::io::ErrorKind::NotFound,
218 "IndustryData.txt not found",
219 )))
220}
221
222pub fn parse_holiday_file(path: impl AsRef<Path>) -> Result<Vec<i64>, MetadataParseError> {
224 let text = fs::read_to_string(path)?;
225 let mut out = Vec::new();
226 for line in text.lines() {
227 let line = line.trim();
228 if line.is_empty() {
229 continue;
230 }
231 let Some(first) = line.split(',').next() else {
232 continue;
233 };
234 let Some(day) = parse_holiday_token(first) else {
235 continue;
236 };
237 out.push(day);
238 }
239 if out.is_empty() {
240 return Err(MetadataParseError::NoRecords("holiday"));
241 }
242 Ok(out)
243}
244
245pub fn load_holidays_from_standard_paths() -> Result<Vec<i64>, MetadataParseError> {
247 resolve_holiday_csv()
248 .and_then(parse_holiday_file)
249 .or_else(|_| resolve_holiday_dat().and_then(parse_holiday_file))
250}
251
252pub fn load_holidays_from_root(root: impl AsRef<Path>) -> Result<Vec<i64>, MetadataParseError> {
254 let roots = vec![root.as_ref().to_path_buf()];
255 resolve_holiday_csv_from_roots(&roots)
256 .and_then(parse_holiday_file)
257 .or_else(|_| resolve_holiday_dat_from_roots(&roots).and_then(parse_holiday_file))
258}
259
260pub fn parse_sector_name_file(path: impl AsRef<Path>) -> Result<Vec<String>, MetadataParseError> {
262 let text = fs::read_to_string(path)?;
263 let mut out = text
264 .lines()
265 .map(str::trim)
266 .filter(|line| !line.is_empty())
267 .filter_map(|line| line.split(';').next().map(str::trim))
268 .filter(|name| !name.is_empty())
269 .map(ToString::to_string)
270 .collect::<Vec<_>>();
271 out.sort();
272 out.dedup();
273 if out.is_empty() {
274 return Err(MetadataParseError::NoRecords("sector names"));
275 }
276 Ok(out)
277}
278
279pub fn load_sector_names_from_standard_paths() -> Result<Vec<String>, MetadataParseError> {
281 let paths = resolve_sector_name_files();
282 if paths.is_empty() {
283 return load_sectorlist_from_standard_paths();
284 }
285 let mut out = Vec::new();
286 for path in paths {
287 out.extend(parse_sector_name_file(path)?);
288 }
289 out.sort();
290 out.dedup();
291 if out.is_empty() {
292 return Err(MetadataParseError::NoRecords("sector names"));
293 }
294 Ok(out)
295}
296
297pub fn load_sector_names_from_root(
299 root: impl AsRef<Path>,
300) -> Result<Vec<String>, MetadataParseError> {
301 let roots = vec![root.as_ref().to_path_buf()];
302 let paths = resolve_sector_name_files_from_roots(&roots);
303 if paths.is_empty() {
304 return load_sectorlist_from_root(root);
305 }
306 let mut out = Vec::new();
307 for path in paths {
308 out.extend(parse_sector_name_file(path)?);
309 }
310 out.sort();
311 out.dedup();
312 if out.is_empty() {
313 return Err(MetadataParseError::NoRecords("sector names"));
314 }
315 Ok(out)
316}
317
318pub fn parse_sectorlist_dat(path: impl AsRef<Path>) -> Result<Vec<String>, MetadataParseError> {
320 let bytes = fs::read(path)?;
321 let text = String::from_utf8_lossy(&bytes);
322 let mut out = text
323 .lines()
324 .map(str::trim)
325 .filter(|line| !line.is_empty())
326 .map(ToString::to_string)
327 .collect::<Vec<_>>();
328 out.sort();
329 out.dedup();
330 if out.is_empty() {
331 return Err(MetadataParseError::NoRecords("sectorlist"));
332 }
333 Ok(out)
334}
335
336pub fn load_sectorlist_from_standard_paths() -> Result<Vec<String>, MetadataParseError> {
338 parse_sectorlist_dat(resolve_sectorlist_dat()?)
339}
340
341pub fn load_sectorlist_from_root(
343 root: impl AsRef<Path>,
344) -> Result<Vec<String>, MetadataParseError> {
345 let roots = vec![root.as_ref().to_path_buf()];
346 parse_sectorlist_dat(resolve_sectorlist_dat_from_roots(&roots)?)
347}
348
349pub fn parse_sector_weight_members(
351 path: impl AsRef<Path>,
352) -> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
353 let text = fs::read_to_string(path)?;
354 let mut out = BTreeMap::new();
355
356 for line in text.lines() {
357 let line = line.trim();
358 if line.is_empty() {
359 continue;
360 }
361 let Some((sector, entries)) = parse_sector_weight_line(line) else {
362 continue;
363 };
364 let mut stocks = Vec::new();
365 for (stock_code, _weight) in entries {
366 stocks.push(stock_code);
367 }
368 stocks.sort();
369 stocks.dedup();
370 out.insert(sector, stocks);
371 }
372
373 if out.is_empty() {
374 return Err(MetadataParseError::NoRecords("sector weight members"));
375 }
376 Ok(out)
377}
378
379pub fn load_sector_weight_members_from_standard_paths()
381-> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
382 parse_sector_weight_members(resolve_sector_weight_file()?)
383}
384
385pub fn load_sector_weight_members_from_root(
387 root: impl AsRef<Path>,
388) -> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
389 let roots = vec![root.as_ref().to_path_buf()];
390 parse_sector_weight_members(resolve_sector_weight_file_from_roots(&roots)?)
391}
392
393pub fn parse_sector_weight_index(
395 path: impl AsRef<Path>,
396 index_code: &str,
397) -> Result<BTreeMap<String, f64>, MetadataParseError> {
398 let text = fs::read_to_string(path)?;
399 let mut out = BTreeMap::new();
400
401 for line in text.lines() {
402 let line = line.trim();
403 if line.is_empty() {
404 continue;
405 }
406 let Some((sector, entries)) = parse_sector_weight_line(line) else {
407 continue;
408 };
409 if !sector.eq_ignore_ascii_case(index_code) {
410 continue;
411 }
412 for (stock_code, weight) in entries {
413 out.insert(stock_code, weight);
414 }
415 }
416
417 if out.is_empty() {
418 return Err(MetadataParseError::NoRecords("sector weight index"));
419 }
420 Ok(out)
421}
422
423pub fn load_sector_weight_index_from_standard_paths(
425 index_code: &str,
426) -> Result<BTreeMap<String, f64>, MetadataParseError> {
427 parse_sector_weight_index(resolve_sector_weight_file()?, index_code)
428}
429
430pub fn load_sector_weight_index_from_root(
432 root: impl AsRef<Path>,
433 index_code: &str,
434) -> Result<BTreeMap<String, f64>, MetadataParseError> {
435 let roots = vec![root.as_ref().to_path_buf()];
436 parse_sector_weight_index(resolve_sector_weight_file_from_roots(&roots)?, index_code)
437}
438
439fn parse_sector_weight_line(line: &str) -> Option<(String, Vec<(String, f64)>)> {
440 let parts = line
441 .split(';')
442 .map(str::trim)
443 .filter(|part| !part.is_empty())
444 .collect::<Vec<_>>();
445 if parts.len() < 2 {
446 return None;
447 }
448
449 let sector = parts[0].to_ascii_uppercase();
450 let mut entries = Vec::new();
451 let mut i = 1;
452
453 while i < parts.len() {
454 if let Some((stock_code, weight)) = parse_compact_weight_entry(parts[i]) {
455 entries.push((stock_code, weight));
456 i += 1;
457 continue;
458 }
459
460 if i + 1 >= parts.len() {
461 break;
462 }
463
464 let stock_code = parts[i].trim();
465 let Ok(weight) = parts[i + 1].trim().parse::<f64>() else {
466 i += 1;
467 continue;
468 };
469 if !stock_code.is_empty() {
470 entries.push((stock_code.to_ascii_uppercase(), weight));
471 }
472 i += 2;
473 }
474
475 if entries.is_empty() {
476 return None;
477 }
478
479 Some((sector, entries))
480}
481
482fn parse_compact_weight_entry(entry: &str) -> Option<(String, f64)> {
483 let (stock_code, weight) = entry.split_once(',')?;
484 let stock_code = stock_code.trim();
485 let weight = weight.trim().parse::<f64>().ok()?;
486 if stock_code.is_empty() {
487 return None;
488 }
489 Some((stock_code.to_ascii_uppercase(), weight))
490}
491
492pub fn parse_industry_file(
494 path: impl AsRef<Path>,
495) -> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
496 let text = fs::read_to_string(path)?;
497 let mut out = BTreeMap::new();
498
499 for line in text.lines() {
500 let line = line.trim();
501 if line.is_empty() {
502 continue;
503 }
504 let parts = line
505 .split(',')
506 .map(str::trim)
507 .filter(|part| !part.is_empty())
508 .collect::<Vec<_>>();
509 if parts.len() < 2 {
510 continue;
511 }
512 let industry = parts[0].to_string();
513 let mut stocks = parts[1..]
514 .iter()
515 .map(|s| s.to_ascii_uppercase())
516 .collect::<Vec<_>>();
517 stocks.sort();
518 stocks.dedup();
519 out.insert(industry, stocks);
520 }
521
522 if out.is_empty() {
523 return Err(MetadataParseError::NoRecords("industry"));
524 }
525 Ok(out)
526}
527
528pub fn load_industry_from_standard_paths()
530-> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
531 parse_industry_file(resolve_industry_file()?)
532}
533
534pub fn load_industry_from_root(
536 root: impl AsRef<Path>,
537) -> Result<BTreeMap<String, Vec<String>>, MetadataParseError> {
538 let roots = vec![root.as_ref().to_path_buf()];
539 parse_industry_file(resolve_industry_file_from_roots(&roots)?)
540}