using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// KRX OpenAPI(data-dbg.krx.co.kr) ESG 증권상품(esg/esg_etp_info) 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: { "OutBlock_1": [ { BAS_DD, ISU_ABBRV, TDD_CLSPRC, CMPPREVDD_PRC, FLUC_RT, LIST_SHRS, ACC_TRDVOL, ACC_TRDVAL }, ... ] }. /// 모든 값은 문자열이며 천단위 콤마 포함 가능. 등락률(FLUC_RT)은 소수 % 그대로 저장한다 ("-.43"/".06" 형태도 decimal 로 흡수, Bp 아님). /// ISU_CD 가 없고 종목명(ISU_ABBRV)만 제공되므로 종목명이 UQ 키의 일부가 된다. EtpDailyTrade(etp) 와 shape 이 달라 별도 파서. /// public static class KrxEsgSecurityParser { /// ESG 증권상품 1행 (등락률은 소수 % 그대로 — Bp 아님) public sealed record EsgSecurityRow( string Name, DateOnly TradeDate, decimal Close, decimal ChangeAmount, decimal ChangeRate, long ListedShares, long Volume, long TradeValue ); 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, "ISU_ABBRV"); var tradeDate = ParseDate(GetString(item, "BAS_DD")); if (name.Length == 0 || tradeDate is null) { continue; } rows.Add(new EsgSecurityRow( name, tradeDate.Value, ParseDecimal(GetString(item, "TDD_CLSPRC")), ParseDecimal(GetString(item, "CMPPREVDD_PRC")), ParseDecimal(GetString(item, "FLUC_RT")), ParseLong(GetString(item, "LIST_SHRS")), 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; } private static long ParseLong(string raw) { return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L; } /// 가격/전일비/등락률(%) 문자열 → decimal 그대로. "" 나 "-" 등 파싱 실패는 0. ".06"/"-.43" 형태도 흡수. private static decimal ParseDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m; } }