using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// 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 원문 유지. /// public static class KrxSriBondParser { /// 사회책임투자채권 정보 1행 (날짜/표면이자율은 결측 시 null) 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 ParseDaily(string json) { using var doc = JsonDocument.Parse(json); var rows = new List(); 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 }; } /// 빈 문자열/"-" 은 null, 나머지는 trim 값 private static string? ToNull(string raw) { return raw.Length == 0 || raw == "-" ? null : raw; } /// 기준일자(BAS_DD) — 필수. 파싱 실패는 null(행 제외). private static DateOnly? ParseDate(string raw) { return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null; } /// 상장일/발행일/상환일 — yyyyMMdd. 빈값/오류는 null. 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; } /// 표면이자율(%) — "" 나 "-" 등 파싱 실패는 null (0% 과 구분) private static decimal? ParseNullableDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } }