using System.Globalization; using System.Text.Json; using Domain.Entities.Stocks.ValueObject; namespace Infrastructure.StockData; /// /// KRX OpenAPI(data-dbg.krx.co.kr) 지수 일별시세 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: { "OutBlock_1": [ { BAS_DD, IDX_CLSS, IDX_NM, CLSPRC_IDX, CMPPREVDD_IDX, FLUC_RT, OPNPRC_IDX, HGPRC_IDX, LWPRC_IDX, ACC_TRDVOL, ACC_TRDVAL, MKTCAP }, ... ] }. /// 모든 값은 문자열이며 천단위 콤마 포함 가능, 결측치는 "-" 로 온다. FLUC_RT(%)는 basis point(%×100)로 반올림 저장한다. /// public static class KrxIndexParser { /// 지수 일별시세 1행 (등락률은 Bp = %×100) public sealed record IndexRow( MarketIndexSeries Series, string IndexName, DateOnly TradeDate, decimal Close, decimal ChangeVal, int FlucRateBp, decimal Open, decimal High, decimal Low, long Volume, long Value, long? MarketCap ); public static IReadOnlyList ParseIndexPrices(string json, MarketIndexSeries series) { 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 IndexRow( series, name, tradeDate.Value, ParseDecimal(GetString(item, "CLSPRC_IDX")), ParseDecimal(GetString(item, "CMPPREVDD_IDX")), ParseRateToBp(GetString(item, "FLUC_RT")), ParseDecimal(GetString(item, "OPNPRC_IDX")), ParseDecimal(GetString(item, "HGPRC_IDX")), ParseDecimal(GetString(item, "LWPRC_IDX")), ParseLong(GetString(item, "ACC_TRDVOL")), ParseLong(GetString(item, "ACC_TRDVAL")), ParseNullableLong(GetString(item, "MKTCAP")) )); } 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 decimal ParseDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m; } private static long ParseLong(string raw) { return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L; } private static long? ParseNullableLong(string raw) { return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } }