| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- using System.Globalization;
- using System.Text.Json;
- using Domain.Entities.Stocks.ValueObject;
- namespace Infrastructure.StockData;
- /// <summary>
- /// 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.
- /// </summary>
- public static class KrxStockParser
- {
- /// <summary>종목기본정보 1행 (마스터 upsert 원천). ISU_ABBRV → Name, ISU_ENG_NM → EnglishName, LIST_DD → ListedDate.</summary>
- public sealed record MasterItem(
- string Code,
- string? Isin,
- string Name,
- string? EnglishName,
- StockMarket Market,
- string? SectorName,
- DateOnly ListedDate
- );
- /// <summary>일별매매 1행 (등락률은 소수 % 그대로 — Bp 아님)</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
- );
- /// <summary>일별매매(stk_bydd_trd/ksq_bydd_trd) 파싱. market 은 호출 엔드포인트로 결정 (응답 MKT_NM 은 무시하고 인자 신뢰).</summary>
- public static IReadOnlyList<DailyPriceItem> ParseDailyPrices(string json, StockMarket market)
- {
- using var doc = JsonDocument.Parse(json);
- var rows = new List<DailyPriceItem>();
- 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;
- }
- /// <summary>종목기본정보(stk_isu_base_info/ksq_isu_base_info) 파싱. market 은 호출 엔드포인트로 결정.</summary>
- public static IReadOnlyList<MasterItem> ParseMasterInfo(string json, StockMarket market)
- {
- using var doc = JsonDocument.Parse(json);
- var rows = new List<MasterItem>();
- 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
- };
- }
- /// <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;
- }
- /// <summary>등락률(%) 문자열 → decimal % 그대로. "" 나 "-" 등 파싱 실패는 0 (DataGoKrStockParser.fltRt 와 동일 스케일).</summary>
- private static decimal ParseDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
- }
- }
|