| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- using System.Globalization;
- using System.Text.Json;
- namespace Infrastructure.StockData;
- /// <summary>
- /// KRX OpenAPI(data-dbg.krx.co.kr) 사회책임투자채권 정보(esg/sri_bond_info) 응답 파서 — 순수 C# (System.Text.Json).
- /// 응답 골격: { "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 }, ... ] }.
- /// 가격/거래량이 아니라 발행 정보 스냅샷이다. 날짜(LIST_DD/ISU_DD/REDMPT_DD)는 yyyyMMdd, 미제공/오류 시 null. 금액은 원 단위 long, 표면이자율(ISU_RT)은 decimal %.
- /// 채권 일별매매(bon)와 shape 이 완전히 달라 별도 파서. 표준코드(ISU_CD)는 12자리 ISIN 원문 유지.
- /// </summary>
- public static class KrxSriBondParser
- {
- /// <summary>사회책임투자채권 정보 1행 (날짜/표면이자율은 결측 시 null)</summary>
- public sealed record SriBondRow(
- string Code,
- DateOnly TradeDate,
- string IssuerName,
- string SriBondType,
- string Name,
- DateOnly? ListDate,
- DateOnly? IssueDate,
- DateOnly? RedemptionDate,
- decimal? CouponRate,
- long IssueAmount,
- long ListAmount,
- string? BondType
- );
- public static IReadOnlyList<SriBondRow> ParseDaily(string json)
- {
- using var doc = JsonDocument.Parse(json);
- var rows = new List<SriBondRow>();
- if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
- {
- return rows;
- }
- foreach (var item in block.EnumerateArray())
- {
- var code = ToNull(GetString(item, "ISU_CD"));
- var issuerName = GetString(item, "ISUR_NM");
- var name = GetString(item, "ISU_NM");
- var tradeDate = ParseDate(GetString(item, "BAS_DD"));
- if (code is null || issuerName.Length == 0 || name.Length == 0 || tradeDate is null)
- {
- continue;
- }
- rows.Add(new SriBondRow(
- code,
- tradeDate.Value,
- issuerName,
- GetString(item, "SRI_BND_TP_NM"),
- name,
- ParseNullableDate(GetString(item, "LIST_DD")),
- ParseNullableDate(GetString(item, "ISU_DD")),
- ParseNullableDate(GetString(item, "REDMPT_DD")),
- ParseNullableDecimal(GetString(item, "ISU_RT")),
- ParseLong(GetString(item, "ISU_AMT")),
- ParseLong(GetString(item, "LIST_AMT")),
- ToNull(GetString(item, "BND_TP_NM"))
- ));
- }
- return rows;
- }
- private static string GetString(JsonElement element, string name)
- {
- if (!element.TryGetProperty(name, out var value))
- {
- return string.Empty;
- }
- return value.ValueKind switch
- {
- JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
- JsonValueKind.Number => value.GetRawText(),
- _ => string.Empty
- };
- }
- /// <summary>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
- private static string? ToNull(string raw)
- {
- return raw.Length == 0 || raw == "-" ? null : raw;
- }
- /// <summary>기준일자(BAS_DD) — 필수. 파싱 실패는 null(행 제외).</summary>
- private static DateOnly? ParseDate(string raw)
- {
- return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
- }
- /// <summary>상장일/발행일/상환일 — yyyyMMdd. 빈값/오류는 null.</summary>
- private static DateOnly? ParseNullableDate(string raw)
- {
- return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
- }
- private static long ParseLong(string raw)
- {
- return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L;
- }
- /// <summary>표면이자율(%) — "" 나 "-" 등 파싱 실패는 null (0% 과 구분)</summary>
- private static decimal? ParseNullableDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
- }
- }
|