using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// KRX OpenAPI(data-dbg.krx.co.kr) 채권지수 일별시세(idx/bon_dd_trd) 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: { "OutBlock_1": [ { BAS_DD, BND_IDX_GRP_NM, TOT_EARNG_IDX(+_CMPPREVDD), NETPRC_IDX(+..), ZERO_REINVST_IDX(+..), CALL_REINVST_IDX(+..), MKT_PRC_IDX(+..), AVG_DURATION, AVG_CONVEXITY_PRC, BND_IDX_AVG_YD }, ... ] }. /// 모든 값은 문자열이며 천단위 콤마 포함 가능, 결측치는 "" 또는 "-" 로 온다(일부 지수 그룹은 제로/Call재투자·시장가격 미제공). 결측은 null 로 흡수한다. /// 채권지수는 OHLC/거래량이 없어 IndexDailyPrice 로 재사용 불가 → BondIndexDailyPrice 전용 파서. /// public static class KrxBondIndexParser { /// 채권지수 일별시세 1행 (모든 지수값은 결측 시 null) public sealed record BondIndexRow( string GroupName, DateOnly TradeDate, decimal? TotalEarningIndex, decimal? TotalEarningChange, decimal? NetPriceIndex, decimal? NetPriceChange, decimal? ZeroReinvestIndex, decimal? ZeroReinvestChange, decimal? CallReinvestIndex, decimal? CallReinvestChange, decimal? MarketPriceIndex, decimal? MarketPriceChange, decimal? AvgDuration, decimal? AvgConvexity, decimal? AvgYield ); public static IReadOnlyList ParseBondIndexPrices(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 groupName = GetString(item, "BND_IDX_GRP_NM"); var tradeDate = ParseDate(GetString(item, "BAS_DD")); if (groupName.Length == 0 || tradeDate is null) { continue; } rows.Add(new BondIndexRow( groupName, tradeDate.Value, ParseNullableDecimal(GetString(item, "TOT_EARNG_IDX")), ParseNullableDecimal(GetString(item, "TOT_EARNG_IDX_CMPPREVDD")), ParseNullableDecimal(GetString(item, "NETPRC_IDX")), ParseNullableDecimal(GetString(item, "NETPRC_IDX_CMPPREVDD")), ParseNullableDecimal(GetString(item, "ZERO_REINVST_IDX")), ParseNullableDecimal(GetString(item, "ZERO_REINVST_IDX_CMPPREVDD")), ParseNullableDecimal(GetString(item, "CALL_REINVST_IDX")), ParseNullableDecimal(GetString(item, "CALL_REINVST_IDX_CMPPREVDD")), ParseNullableDecimal(GetString(item, "MKT_PRC_IDX")), ParseNullableDecimal(GetString(item, "MKT_PRC_IDX_CMPPREVDD")), ParseNullableDecimal(GetString(item, "AVG_DURATION")), ParseNullableDecimal(GetString(item, "AVG_CONVEXITY_PRC")), ParseNullableDecimal(GetString(item, "BND_IDX_AVG_YD")) )); } 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 }; } private static DateOnly? ParseDate(string raw) { return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null; } /// 지수값/전일대비/듀레이션·컨벡시티·YTM — "" 나 "-" 등 파싱 실패는 null private static decimal? ParseNullableDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } }