using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// KRX OpenAPI(data-dbg.krx.co.kr) ESG 지수(esg/esg_index_info) 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: { "OutBlock_1": [ { BAS_DD, IDX_NM, CLSPRC_IDX, PRV_DD_CMPR, UPDN_RATE, TRD_ISU_CNT, ACC_TRDVOL, ACC_TRDVAL }, ... ] }. /// 모든 값은 문자열이며 천단위 콤마 포함 가능. 시장/파생 지수와 달리 OHLC·시가총액이 없고 구성종목수(TRD_ISU_CNT)를 제공한다. /// 등락률(UPDN_RATE, %)은 basis point(%×100, 반올림)로 저장한다 (IndexDailyPrice.FlucRateBp 와 동일 규약, "-.39"/".16" 형태도 흡수). /// public static class KrxEsgIndexParser { /// ESG 지수 1행 (등락률은 Bp = %×100) public sealed record EsgIndexRow( string IndexName, DateOnly TradeDate, decimal Close, decimal ChangeVal, int FlucRateBp, int ConstituentCount, long Volume, long Value ); 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 name = GetString(item, "IDX_NM"); var tradeDate = ParseDate(GetString(item, "BAS_DD")); if (name.Length == 0 || tradeDate is null) { continue; } rows.Add(new EsgIndexRow( name, tradeDate.Value, ParseDecimal(GetString(item, "CLSPRC_IDX")), ParseDecimal(GetString(item, "PRV_DD_CMPR")), ParseRateToBp(GetString(item, "UPDN_RATE")), ParseInt(GetString(item, "TRD_ISU_CNT")), ParseLong(GetString(item, "ACC_TRDVOL")), ParseLong(GetString(item, "ACC_TRDVAL")) )); } 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; } /// 등락률(%) 문자열 → basis point (×100, 반올림). "-" 나 파싱 실패는 0. private static int ParseRateToBp(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var pct) ? (int)Math.Round(pct * 100m, MidpointRounding.AwayFromZero) : 0; } private static int ParseInt(string raw) { return int.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0; } private static long ParseLong(string raw) { return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L; } private static decimal ParseDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m; } }