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). "D1 KRX 주식(종목) 데이터 수집". /// 응답 골격: { "OutBlock_1": [ { ...string values }, ... ] }. 모든 값은 문자열, 천단위 콤마 포함 가능, 결측치는 "" 또는 "-". /// 일별매매(stk_bydd_trd/ksq_bydd_trd): ISU_CD 가 6자리 단축코드. FLUC_RT(%)는 소수 % 그대로 저장한다 /// (StockDailyPrice.ChangeRate / Stock.LastChangeRate 이 decimal % 이므로 DataGoKrStockParser 와 동일 스케일 유지 — Bp 변환 안 함). /// 종목기본정보(stk_isu_base_info/ksq_isu_base_info): ISU_SRT_CD 가 6자리 단축코드, ISU_CD 가 표준코드(KR7…) ISIN. /// public static class KrxStockParser { /// 종목기본정보 1행 (마스터 upsert 원천). ISU_ABBRV → Name, ISU_ENG_NM → EnglishName, LIST_DD → ListedDate. public sealed record MasterItem( string Code, string? Isin, string Name, string? EnglishName, StockMarket Market, string? SectorName, DateOnly ListedDate ); /// 일별매매 1행 (등락률은 소수 % 그대로 — Bp 아님) public sealed record DailyPriceItem( string Code, DateOnly TradingDate, int Open, int High, int Low, int Close, long Volume, long TradingValue, int ChangeAmount, decimal ChangeRate, long? MarketCap, long? ListedShares ); /// 일별매매(stk_bydd_trd/ksq_bydd_trd) 파싱. market 은 호출 엔드포인트로 결정 (응답 MKT_NM 은 무시하고 인자 신뢰). public static IReadOnlyList ParseDailyPrices(string json, StockMarket market) { 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 = NormalizeCode(GetString(item, "ISU_CD")); var tradingDate = ParseDate(GetString(item, "BAS_DD")); if (code is null || tradingDate is null) { continue; } rows.Add(new DailyPriceItem( code, tradingDate.Value, ParseInt(GetString(item, "TDD_OPNPRC")), ParseInt(GetString(item, "TDD_HGPRC")), ParseInt(GetString(item, "TDD_LWPRC")), ParseInt(GetString(item, "TDD_CLSPRC")), ParseLong(GetString(item, "ACC_TRDVOL")), ParseLong(GetString(item, "ACC_TRDVAL")), ParseInt(GetString(item, "CMPPREVDD_PRC")), ParseDecimal(GetString(item, "FLUC_RT")), ParseNullableLong(GetString(item, "MKTCAP")), ParseNullableLong(GetString(item, "LIST_SHRS")) )); } return rows; } /// 종목기본정보(stk_isu_base_info/ksq_isu_base_info) 파싱. market 은 호출 엔드포인트로 결정. public static IReadOnlyList ParseMasterInfo(string json, StockMarket market) { 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 = NormalizeCode(GetString(item, "ISU_SRT_CD")); var name = GetString(item, "ISU_ABBRV"); var listedDate = ParseDate(GetString(item, "LIST_DD")); if (code is null || name.Length == 0 || listedDate is null) { continue; } var isin = GetString(item, "ISU_CD"); var englishName = GetString(item, "ISU_ENG_NM"); var sectorName = GetString(item, "SECT_TP_NM"); rows.Add(new MasterItem( code, isin.Length == 12 ? isin : null, name, englishName.Length == 0 ? null : englishName, market, sectorName.Length == 0 ? null : sectorName, listedDate.Value )); } 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 }; } /// 단축코드 정규화 — 'A' 프리픽스 등 6자리 초과분은 뒤 6자리만 취하고, 6자리 숫자만 유효 private static string? NormalizeCode(string raw) { if (raw.Length > 6) { raw = raw[^6..]; } if (raw is not { Length: 6 } || raw.Any(c => !char.IsAsciiDigit(c))) { return null; } return raw; } private static DateOnly? ParseDate(string raw) { return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null; } 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 long? ParseNullableLong(string raw) { return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } /// 등락률(%) 문자열 → decimal % 그대로. "" 나 "-" 등 파싱 실패는 0 (DataGoKrStockParser.fltRt 와 동일 스케일). private static decimal ParseDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m; } }