KrxBondParser.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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": [ { ...string values }, ... ] }. 모든 값은 문자열, 천단위 콤마 가능, 결측치는 "" 또는 "-".
  8. /// 채권은 주식/증권상품과 필드가 다르다:
  9. /// - 가격: CLSPRC/OPNPRC/HGPRC/LWPRC (주식의 TDD_* 아님), 전일대비 CMPPREVDD_PRC(빈값 가능). 등락률·시가총액·상장주식수 미제공.
  10. /// - 수익률(YD): CLSPRC_YD(만기수익률=종가수익률)/OPNPRC_YD/HGPRC_YD/LWPRC_YD — nullable(미체결 시 "").
  11. /// - 국채전문(KtsGovt)만: BND_EXP_TP_NM(만기년수) / GOVBND_ISU_TP_NM(종목구분). 일반/소액은 응답에 없음 → null.
  12. /// 단축코드(ISU_CD)는 12자리 ISIN(예: KR103503GE96) — 6자리 정규화 없이 원문 유지. 시장은 호출 엔드포인트(bondMarket)로 결정.
  13. /// </summary>
  14. public static class KrxBondParser
  15. {
  16. /// <summary>채권 일별매매 1행 (수익률은 % 그대로, 미체결은 null)</summary>
  17. public sealed record BondRow(
  18. BondMarket BondMarket,
  19. string Code,
  20. string Name,
  21. DateOnly TradeDate,
  22. decimal Close,
  23. decimal Open,
  24. decimal High,
  25. decimal Low,
  26. decimal ChangeAmount,
  27. decimal? YieldToMaturity,
  28. decimal? OpenYield,
  29. decimal? HighYield,
  30. decimal? LowYield,
  31. long Volume,
  32. long TradeValue,
  33. string? MaturityYears,
  34. string? IssueType
  35. );
  36. /// <summary>kts/bnd/smb_bydd_trd 파싱. bondMarket 은 호출 엔드포인트로 결정, 국채전문 전용 필드는 조건 분기.</summary>
  37. public static IReadOnlyList<BondRow> ParseDaily(string json, BondMarket bondMarket)
  38. {
  39. using var doc = JsonDocument.Parse(json);
  40. var rows = new List<BondRow>();
  41. if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
  42. {
  43. return rows;
  44. }
  45. foreach (var item in block.EnumerateArray())
  46. {
  47. var code = ToNull(GetString(item, "ISU_CD"));
  48. var name = GetString(item, "ISU_NM");
  49. var tradeDate = ParseDate(GetString(item, "BAS_DD"));
  50. if (code is null || name.Length == 0 || tradeDate is null)
  51. {
  52. continue;
  53. }
  54. string? maturityYears = null;
  55. string? issueType = null;
  56. if (bondMarket == BondMarket.KtsGovt)
  57. {
  58. maturityYears = ToNull(GetString(item, "BND_EXP_TP_NM"));
  59. issueType = ToNull(GetString(item, "GOVBND_ISU_TP_NM"));
  60. }
  61. rows.Add(new BondRow(
  62. bondMarket,
  63. code,
  64. name,
  65. tradeDate.Value,
  66. ParseDecimal(GetString(item, "CLSPRC")),
  67. ParseDecimal(GetString(item, "OPNPRC")),
  68. ParseDecimal(GetString(item, "HGPRC")),
  69. ParseDecimal(GetString(item, "LWPRC")),
  70. ParseDecimal(GetString(item, "CMPPREVDD_PRC")),
  71. ParseNullableDecimal(GetString(item, "CLSPRC_YD")),
  72. ParseNullableDecimal(GetString(item, "OPNPRC_YD")),
  73. ParseNullableDecimal(GetString(item, "HGPRC_YD")),
  74. ParseNullableDecimal(GetString(item, "LWPRC_YD")),
  75. ParseLong(GetString(item, "ACC_TRDVOL")),
  76. ParseLong(GetString(item, "ACC_TRDVAL")),
  77. maturityYears,
  78. issueType
  79. ));
  80. }
  81. return rows;
  82. }
  83. private static string GetString(JsonElement element, string name)
  84. {
  85. if (!element.TryGetProperty(name, out var value))
  86. {
  87. return string.Empty;
  88. }
  89. return value.ValueKind switch
  90. {
  91. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  92. JsonValueKind.Number => value.GetRawText(),
  93. _ => string.Empty
  94. };
  95. }
  96. /// <summary>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
  97. private static string? ToNull(string raw)
  98. {
  99. return raw.Length == 0 || raw == "-" ? null : raw;
  100. }
  101. private static DateOnly? ParseDate(string raw)
  102. {
  103. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  104. }
  105. private static long ParseLong(string raw)
  106. {
  107. return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L;
  108. }
  109. /// <summary>가격/전일대비 문자열 → decimal 그대로. "" 나 "-" 등 파싱 실패는 0.</summary>
  110. private static decimal ParseDecimal(string raw)
  111. {
  112. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
  113. }
  114. /// <summary>수익률(YD) — "" 나 "-" 등 파싱 실패는 null (0 과 구분, 미체결은 값 없음)</summary>
  115. private static decimal? ParseNullableDecimal(string raw)
  116. {
  117. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  118. }
  119. }