KrxStockParser.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. using System.Globalization;
  2. using System.Text.Json;
  3. using Domain.Entities.Stocks.ValueObject;
  4. namespace Infrastructure.StockData;
  5. /// <summary>
  6. /// KRX OpenAPI(data-dbg.krx.co.kr) 주식(종목) 응답 파서 — 순수 C# (System.Text.Json). "D1 KRX 주식(종목) 데이터 수집".
  7. /// 응답 골격: { "OutBlock_1": [ { ...string values }, ... ] }. 모든 값은 문자열, 천단위 콤마 포함 가능, 결측치는 "" 또는 "-".
  8. /// 일별매매(stk_bydd_trd/ksq_bydd_trd): ISU_CD 가 6자리 단축코드. FLUC_RT(%)는 소수 % 그대로 저장한다
  9. /// (StockDailyPrice.ChangeRate / Stock.LastChangeRate 이 decimal % 이므로 DataGoKrStockParser 와 동일 스케일 유지 — Bp 변환 안 함).
  10. /// 종목기본정보(stk_isu_base_info/ksq_isu_base_info): ISU_SRT_CD 가 6자리 단축코드, ISU_CD 가 표준코드(KR7…) ISIN.
  11. /// </summary>
  12. public static class KrxStockParser
  13. {
  14. /// <summary>종목기본정보 1행 (마스터 upsert 원천). ISU_ABBRV → Name, ISU_ENG_NM → EnglishName, LIST_DD → ListedDate.</summary>
  15. public sealed record MasterItem(
  16. string Code,
  17. string? Isin,
  18. string Name,
  19. string? EnglishName,
  20. StockMarket Market,
  21. string? SectorName,
  22. DateOnly ListedDate
  23. );
  24. /// <summary>일별매매 1행 (등락률은 소수 % 그대로 — Bp 아님)</summary>
  25. public sealed record DailyPriceItem(
  26. string Code,
  27. DateOnly TradingDate,
  28. int Open,
  29. int High,
  30. int Low,
  31. int Close,
  32. long Volume,
  33. long TradingValue,
  34. int ChangeAmount,
  35. decimal ChangeRate,
  36. long? MarketCap,
  37. long? ListedShares
  38. );
  39. /// <summary>일별매매(stk_bydd_trd/ksq_bydd_trd) 파싱. market 은 호출 엔드포인트로 결정 (응답 MKT_NM 은 무시하고 인자 신뢰).</summary>
  40. public static IReadOnlyList<DailyPriceItem> ParseDailyPrices(string json, StockMarket market)
  41. {
  42. using var doc = JsonDocument.Parse(json);
  43. var rows = new List<DailyPriceItem>();
  44. if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
  45. {
  46. return rows;
  47. }
  48. foreach (var item in block.EnumerateArray())
  49. {
  50. var code = NormalizeCode(GetString(item, "ISU_CD"));
  51. var tradingDate = ParseDate(GetString(item, "BAS_DD"));
  52. if (code is null || tradingDate is null)
  53. {
  54. continue;
  55. }
  56. rows.Add(new DailyPriceItem(
  57. code,
  58. tradingDate.Value,
  59. ParseInt(GetString(item, "TDD_OPNPRC")),
  60. ParseInt(GetString(item, "TDD_HGPRC")),
  61. ParseInt(GetString(item, "TDD_LWPRC")),
  62. ParseInt(GetString(item, "TDD_CLSPRC")),
  63. ParseLong(GetString(item, "ACC_TRDVOL")),
  64. ParseLong(GetString(item, "ACC_TRDVAL")),
  65. ParseInt(GetString(item, "CMPPREVDD_PRC")),
  66. ParseDecimal(GetString(item, "FLUC_RT")),
  67. ParseNullableLong(GetString(item, "MKTCAP")),
  68. ParseNullableLong(GetString(item, "LIST_SHRS"))
  69. ));
  70. }
  71. return rows;
  72. }
  73. /// <summary>종목기본정보(stk_isu_base_info/ksq_isu_base_info) 파싱. market 은 호출 엔드포인트로 결정.</summary>
  74. public static IReadOnlyList<MasterItem> ParseMasterInfo(string json, StockMarket market)
  75. {
  76. using var doc = JsonDocument.Parse(json);
  77. var rows = new List<MasterItem>();
  78. if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
  79. {
  80. return rows;
  81. }
  82. foreach (var item in block.EnumerateArray())
  83. {
  84. var code = NormalizeCode(GetString(item, "ISU_SRT_CD"));
  85. var name = GetString(item, "ISU_ABBRV");
  86. var listedDate = ParseDate(GetString(item, "LIST_DD"));
  87. if (code is null || name.Length == 0 || listedDate is null)
  88. {
  89. continue;
  90. }
  91. var isin = GetString(item, "ISU_CD");
  92. var englishName = GetString(item, "ISU_ENG_NM");
  93. var sectorName = GetString(item, "SECT_TP_NM");
  94. rows.Add(new MasterItem(
  95. code,
  96. isin.Length == 12 ? isin : null,
  97. name,
  98. englishName.Length == 0 ? null : englishName,
  99. market,
  100. sectorName.Length == 0 ? null : sectorName,
  101. listedDate.Value
  102. ));
  103. }
  104. return rows;
  105. }
  106. private static string GetString(JsonElement element, string name)
  107. {
  108. if (!element.TryGetProperty(name, out var value))
  109. {
  110. return string.Empty;
  111. }
  112. return value.ValueKind switch
  113. {
  114. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  115. JsonValueKind.Number => value.GetRawText(),
  116. _ => string.Empty
  117. };
  118. }
  119. /// <summary>단축코드 정규화 — 'A' 프리픽스 등 6자리 초과분은 뒤 6자리만 취하고, 6자리 숫자만 유효</summary>
  120. private static string? NormalizeCode(string raw)
  121. {
  122. if (raw.Length > 6)
  123. {
  124. raw = raw[^6..];
  125. }
  126. if (raw is not { Length: 6 } || raw.Any(c => !char.IsAsciiDigit(c)))
  127. {
  128. return null;
  129. }
  130. return raw;
  131. }
  132. private static DateOnly? ParseDate(string raw)
  133. {
  134. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  135. }
  136. private static int ParseInt(string raw)
  137. {
  138. return int.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0;
  139. }
  140. private static long ParseLong(string raw)
  141. {
  142. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L;
  143. }
  144. private static long? ParseNullableLong(string raw)
  145. {
  146. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  147. }
  148. /// <summary>등락률(%) 문자열 → decimal % 그대로. "" 나 "-" 등 파싱 실패는 0 (DataGoKrStockParser.fltRt 와 동일 스케일).</summary>
  149. private static decimal ParseDecimal(string raw)
  150. {
  151. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
  152. }
  153. }