WorldIndexCsvParser.cs 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. using System.Globalization;
  2. namespace Infrastructure.StockData;
  3. /// <summary>
  4. /// Stooq 라이트 배치 시세(/q/l/?f=sd2t2ohlc&amp;h&amp;e=csv) CSV 파서 — 순수 C#.
  5. /// 헤더: Symbol,Date,Time,Open,High,Low,Close. 값 결측은 "N/D" 로 온다(장 시작 전·심볼 오류 등).
  6. /// Close 가 유효하지 않은 행은 버리고, Open/High/Low 결측은 Close 로 보정한다.
  7. /// </summary>
  8. public static class WorldIndexCsvParser
  9. {
  10. /// <summary>세계지수 시세 1행 (외부 소스 최신 종가 기준)</summary>
  11. public sealed record WorldIndexQuote(
  12. string Symbol,
  13. DateOnly TradeDate,
  14. decimal Open,
  15. decimal High,
  16. decimal Low,
  17. decimal Close
  18. );
  19. public static IReadOnlyList<WorldIndexQuote> Parse(string csv)
  20. {
  21. var rows = new List<WorldIndexQuote>();
  22. if (string.IsNullOrWhiteSpace(csv))
  23. {
  24. return rows;
  25. }
  26. var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  27. foreach (var line in lines)
  28. {
  29. var cols = line.Split(',');
  30. // 헤더 행(Symbol,Date,...) 및 컬럼 부족 행 skip
  31. if (cols.Length < 7 || cols[0].Trim().Equals("Symbol", StringComparison.OrdinalIgnoreCase))
  32. {
  33. continue;
  34. }
  35. var symbol = cols[0].Trim();
  36. var tradeDate = ParseDate(cols[1]);
  37. var close = ParseDecimal(cols[6]);
  38. if (symbol.Length == 0 || tradeDate is null || close is null)
  39. {
  40. continue;
  41. }
  42. // OHLC 결측(N/D)은 종가로 보정
  43. var open = ParseDecimal(cols[3]) ?? close.Value;
  44. var high = ParseDecimal(cols[4]) ?? close.Value;
  45. var low = ParseDecimal(cols[5]) ?? close.Value;
  46. rows.Add(new WorldIndexQuote(symbol.ToLowerInvariant(), tradeDate.Value, open, high, low, close.Value));
  47. }
  48. return rows;
  49. }
  50. private static DateOnly? ParseDate(string raw)
  51. {
  52. return DateOnly.TryParseExact(raw.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  53. }
  54. /// <summary>Stooq 수치 파싱 — "N/D"·빈값·파싱 실패는 null.</summary>
  55. private static decimal? ParseDecimal(string raw)
  56. {
  57. return decimal.TryParse(raw.Trim(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  58. }
  59. }