KrxSriBondParser.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. using System.Globalization;
  2. using System.Text.Json;
  3. namespace Infrastructure.StockData;
  4. /// <summary>
  5. /// KRX OpenAPI(data-dbg.krx.co.kr) 사회책임투자채권 정보(esg/sri_bond_info) 응답 파서 — 순수 C# (System.Text.Json).
  6. /// 응답 골격: { "OutBlock_1": [ { BAS_DD, ISUR_NM, ISU_CD, SRI_BND_TP_NM, ISU_NM, LIST_DD, ISU_DD, REDMPT_DD, ISU_RT, ISU_AMT, LIST_AMT, BND_TP_NM }, ... ] }.
  7. /// 가격/거래량이 아니라 발행 정보 스냅샷이다. 날짜(LIST_DD/ISU_DD/REDMPT_DD)는 yyyyMMdd, 미제공/오류 시 null. 금액은 원 단위 long, 표면이자율(ISU_RT)은 decimal %.
  8. /// 채권 일별매매(bon)와 shape 이 완전히 달라 별도 파서. 표준코드(ISU_CD)는 12자리 ISIN 원문 유지.
  9. /// </summary>
  10. public static class KrxSriBondParser
  11. {
  12. /// <summary>사회책임투자채권 정보 1행 (날짜/표면이자율은 결측 시 null)</summary>
  13. public sealed record SriBondRow(
  14. string Code,
  15. DateOnly TradeDate,
  16. string IssuerName,
  17. string SriBondType,
  18. string Name,
  19. DateOnly? ListDate,
  20. DateOnly? IssueDate,
  21. DateOnly? RedemptionDate,
  22. decimal? CouponRate,
  23. long IssueAmount,
  24. long ListAmount,
  25. string? BondType
  26. );
  27. public static IReadOnlyList<SriBondRow> ParseDaily(string json)
  28. {
  29. using var doc = JsonDocument.Parse(json);
  30. var rows = new List<SriBondRow>();
  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 code = ToNull(GetString(item, "ISU_CD"));
  38. var issuerName = GetString(item, "ISUR_NM");
  39. var name = GetString(item, "ISU_NM");
  40. var tradeDate = ParseDate(GetString(item, "BAS_DD"));
  41. if (code is null || issuerName.Length == 0 || name.Length == 0 || tradeDate is null)
  42. {
  43. continue;
  44. }
  45. rows.Add(new SriBondRow(
  46. code,
  47. tradeDate.Value,
  48. issuerName,
  49. GetString(item, "SRI_BND_TP_NM"),
  50. name,
  51. ParseNullableDate(GetString(item, "LIST_DD")),
  52. ParseNullableDate(GetString(item, "ISU_DD")),
  53. ParseNullableDate(GetString(item, "REDMPT_DD")),
  54. ParseNullableDecimal(GetString(item, "ISU_RT")),
  55. ParseLong(GetString(item, "ISU_AMT")),
  56. ParseLong(GetString(item, "LIST_AMT")),
  57. ToNull(GetString(item, "BND_TP_NM"))
  58. ));
  59. }
  60. return rows;
  61. }
  62. private static string GetString(JsonElement element, string name)
  63. {
  64. if (!element.TryGetProperty(name, out var value))
  65. {
  66. return string.Empty;
  67. }
  68. return value.ValueKind switch
  69. {
  70. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  71. JsonValueKind.Number => value.GetRawText(),
  72. _ => string.Empty
  73. };
  74. }
  75. /// <summary>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
  76. private static string? ToNull(string raw)
  77. {
  78. return raw.Length == 0 || raw == "-" ? null : raw;
  79. }
  80. /// <summary>기준일자(BAS_DD) — 필수. 파싱 실패는 null(행 제외).</summary>
  81. private static DateOnly? ParseDate(string raw)
  82. {
  83. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  84. }
  85. /// <summary>상장일/발행일/상환일 — yyyyMMdd. 빈값/오류는 null.</summary>
  86. private static DateOnly? ParseNullableDate(string raw)
  87. {
  88. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  89. }
  90. private static long ParseLong(string raw)
  91. {
  92. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L;
  93. }
  94. /// <summary>표면이자율(%) — "" 나 "-" 등 파싱 실패는 null (0% 과 구분)</summary>
  95. private static decimal? ParseNullableDecimal(string raw)
  96. {
  97. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  98. }
  99. }