KrxIndexParser.cs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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).
  7. /// 응답 골격: { "OutBlock_1": [ { BAS_DD, IDX_CLSS, IDX_NM, CLSPRC_IDX, CMPPREVDD_IDX, FLUC_RT, OPNPRC_IDX, HGPRC_IDX, LWPRC_IDX, ACC_TRDVOL, ACC_TRDVAL, MKTCAP }, ... ] }.
  8. /// 모든 값은 문자열이며 천단위 콤마 포함 가능, 결측치는 "-" 로 온다. FLUC_RT(%)는 basis point(%×100)로 반올림 저장한다.
  9. /// </summary>
  10. public static class KrxIndexParser
  11. {
  12. /// <summary>지수 일별시세 1행 (등락률은 Bp = %×100)</summary>
  13. public sealed record IndexRow(
  14. MarketIndexSeries Series,
  15. string IndexName,
  16. DateOnly TradeDate,
  17. decimal Close,
  18. decimal ChangeVal,
  19. int FlucRateBp,
  20. decimal Open,
  21. decimal High,
  22. decimal Low,
  23. long Volume,
  24. long Value,
  25. long? MarketCap
  26. );
  27. public static IReadOnlyList<IndexRow> ParseIndexPrices(string json, MarketIndexSeries series)
  28. {
  29. using var doc = JsonDocument.Parse(json);
  30. var rows = new List<IndexRow>();
  31. if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
  32. {
  33. return rows;
  34. }
  35. foreach (var item in block.EnumerateArray())
  36. {
  37. var name = GetString(item, "IDX_NM");
  38. var tradeDate = ParseDate(GetString(item, "BAS_DD"));
  39. if (name.Length == 0 || tradeDate is null)
  40. {
  41. continue;
  42. }
  43. rows.Add(new IndexRow(
  44. series,
  45. name,
  46. tradeDate.Value,
  47. ParseDecimal(GetString(item, "CLSPRC_IDX")),
  48. ParseDecimal(GetString(item, "CMPPREVDD_IDX")),
  49. ParseRateToBp(GetString(item, "FLUC_RT")),
  50. ParseDecimal(GetString(item, "OPNPRC_IDX")),
  51. ParseDecimal(GetString(item, "HGPRC_IDX")),
  52. ParseDecimal(GetString(item, "LWPRC_IDX")),
  53. ParseLong(GetString(item, "ACC_TRDVOL")),
  54. ParseLong(GetString(item, "ACC_TRDVAL")),
  55. ParseNullableLong(GetString(item, "MKTCAP"))
  56. ));
  57. }
  58. return rows;
  59. }
  60. private static string GetString(JsonElement element, string name)
  61. {
  62. if (!element.TryGetProperty(name, out var value))
  63. {
  64. return string.Empty;
  65. }
  66. return value.ValueKind switch
  67. {
  68. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  69. JsonValueKind.Number => value.GetRawText(),
  70. _ => string.Empty
  71. };
  72. }
  73. private static DateOnly? ParseDate(string raw)
  74. {
  75. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  76. }
  77. /// <summary>등락률(%) 문자열 → basis point (×100, 반올림). "-" 나 파싱 실패는 0.</summary>
  78. private static int ParseRateToBp(string raw)
  79. {
  80. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var pct)
  81. ? (int)Math.Round(pct * 100m, MidpointRounding.AwayFromZero)
  82. : 0;
  83. }
  84. private static decimal ParseDecimal(string raw)
  85. {
  86. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
  87. }
  88. private static long ParseLong(string raw)
  89. {
  90. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L;
  91. }
  92. private static long? ParseNullableLong(string raw)
  93. {
  94. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  95. }
  96. }