| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- using System.Globalization;
- using System.Text.Json;
- namespace Infrastructure.StockData;
- /// <summary>
- /// 공공데이터포털(금융위) 표준 JSON 응답 파서 — 순수 C# (System.Text.Json).
- /// 응답 골격: response.header(resultCode/resultMsg) + response.body(totalCount, items.item[]).
- /// item 이 단건이면 객체, 0건이면 items 가 빈 문자열로 오는 케이스까지 흡수한다.
- /// </summary>
- public static class DataGoKrStockParser
- {
- /// <summary>KRX 상장종목정보 (GetKrxListedInfoService/getItemInfo) 1행</summary>
- public sealed record ListedItem(string Code, string? Isin, string Name, string MarketName, DateOnly BaseDate);
- /// <summary>주식시세정보 (GetStockSecuritiesInfoService/getStockPriceInfo) 1행</summary>
- 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
- );
- public static (IReadOnlyList<ListedItem> Items, int TotalCount) ParseListedItems(string json)
- {
- using var doc = JsonDocument.Parse(json);
- var body = GetBody(doc);
- var items = new List<ListedItem>();
- foreach (var item in EnumerateItems(body))
- {
- var code = NormalizeCode(GetString(item, "srtnCd"));
- var name = GetString(item, "itmsNm");
- var market = GetString(item, "mrktCtg");
- var baseDate = ParseDate(GetString(item, "basDt"));
- if (code is null || name.Length == 0 || baseDate is null)
- {
- continue;
- }
- var isin = GetString(item, "isinCd");
- items.Add(new ListedItem(code, isin.Length == 12 ? isin : null, name, market, baseDate.Value));
- }
- return (items, GetTotalCount(body));
- }
- public static (IReadOnlyList<DailyPriceItem> Items, int TotalCount) ParseDailyPrices(string json)
- {
- using var doc = JsonDocument.Parse(json);
- var body = GetBody(doc);
- var items = new List<DailyPriceItem>();
- foreach (var item in EnumerateItems(body))
- {
- var code = NormalizeCode(GetString(item, "srtnCd"));
- var tradingDate = ParseDate(GetString(item, "basDt"));
- if (code is null || tradingDate is null)
- {
- continue;
- }
- items.Add(new DailyPriceItem(
- code,
- tradingDate.Value,
- ParseInt(GetString(item, "mkp")),
- ParseInt(GetString(item, "hipr")),
- ParseInt(GetString(item, "lopr")),
- ParseInt(GetString(item, "clpr")),
- ParseLong(GetString(item, "trqu")),
- ParseLong(GetString(item, "trPrc")),
- ParseInt(GetString(item, "vs")),
- ParseDecimal(GetString(item, "fltRt")),
- ParseNullableLong(GetString(item, "mrktTotAmt")),
- ParseNullableLong(GetString(item, "lstgStCnt"))
- ));
- }
- return (items, GetTotalCount(body));
- }
- private static JsonElement GetBody(JsonDocument doc)
- {
- if (!doc.RootElement.TryGetProperty("response", out var response))
- {
- throw new InvalidOperationException("data.go.kr 응답 형식 오류 — response 노드 없음");
- }
- if (response.TryGetProperty("header", out var header))
- {
- var resultCode = header.TryGetProperty("resultCode", out var rc) ? rc.GetString() : null;
- if (resultCode is not null && resultCode != "00")
- {
- var resultMsg = header.TryGetProperty("resultMsg", out var rm) ? rm.GetString() : null;
- throw new InvalidOperationException($"data.go.kr 오류 응답 — resultCode={resultCode}, resultMsg={resultMsg}");
- }
- }
- if (!response.TryGetProperty("body", out var body))
- {
- throw new InvalidOperationException("data.go.kr 응답 형식 오류 — body 노드 없음");
- }
- return body;
- }
- private static IEnumerable<JsonElement> EnumerateItems(JsonElement body)
- {
- // 0건이면 items 가 "" (빈 문자열) 로 온다
- if (!body.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Object)
- {
- yield break;
- }
- if (!items.TryGetProperty("item", out var item))
- {
- yield break;
- }
- if (item.ValueKind == JsonValueKind.Array)
- {
- foreach (var element in item.EnumerateArray())
- {
- yield return element;
- }
- }
- else if (item.ValueKind == JsonValueKind.Object)
- {
- // 단건이면 배열이 아닌 객체로 온다
- yield return item;
- }
- }
- private static int GetTotalCount(JsonElement body)
- {
- if (!body.TryGetProperty("totalCount", out var total))
- {
- return 0;
- }
- return total.ValueKind switch
- {
- JsonValueKind.Number => total.GetInt32(),
- JsonValueKind.String => ParseInt(total.GetString() ?? ""),
- _ => 0
- };
- }
- 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
- };
- }
- /// <summary>단축코드 정규화 — 'A' 프리픽스 등 6자리 초과분은 뒤 6자리만 취하고, 6자리 숫자만 유효</summary>
- 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;
- }
- private static decimal ParseDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : 0m;
- }
- }
|