| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- 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).
- /// 응답 골격: { "OutBlock_1": [ { ...string values }, ... ] }. 모든 값은 문자열, 천단위 콤마 가능, 결측치는 "" 또는 "-".
- /// 시장별 응답 shape 이 다르다:
- /// - 금(gold)/배출권(ets): ISU_CD/ISU_NM + TDD_CLSPRC/OPNPRC/HGPRC/LWPRC(주식형 OHLC) + CMPPREVDD_PRC(전일대비)·FLUC_RT(등락률).
- /// - 석유(oil): ISU_CD 없음 → OIL_NM(유종구분)을 Code·Name 으로 사용. OHLC 미제공(0), 전일대비·등락률 없음(null).
- /// 가격은 WT_AVG_PRC(가중평균가격_경쟁)를 Close 로, WT_DIS_AVG_PRC(가중평균가격_협의)를 WeightedDiscussionPrice 로.
- /// 시장은 호출 엔드포인트(commodityMarket)로 결정.
- /// </summary>
- public static class KrxCommodityParser
- {
- /// <summary>일반상품 일별매매 1행 (등락은 금/배출권만, 협의가격은 석유만 — 나머지는 null)</summary>
- public sealed record CommodityRow(
- CommodityMarket CommodityMarket,
- string Code,
- string Name,
- DateOnly TradeDate,
- decimal Close,
- decimal Open,
- decimal High,
- decimal Low,
- decimal? ChangeAmount,
- decimal? ChangeRate,
- decimal? WeightedDiscussionPrice,
- long Volume,
- long TradeValue
- );
- /// <summary>oil/gold/ets_bydd_trd 파싱. commodityMarket 은 호출 엔드포인트로 결정, 석유 전용/금·배출권 전용 필드는 조건 분기.</summary>
- public static IReadOnlyList<CommodityRow> ParseDaily(string json, CommodityMarket commodityMarket)
- {
- using var doc = JsonDocument.Parse(json);
- var rows = new List<CommodityRow>();
- if (!doc.RootElement.TryGetProperty("OutBlock_1", out var block) || block.ValueKind != JsonValueKind.Array)
- {
- return rows;
- }
- foreach (var item in block.EnumerateArray())
- {
- var tradeDate = ParseDate(GetString(item, "BAS_DD"));
- if (tradeDate is null)
- {
- continue;
- }
- if (commodityMarket == CommodityMarket.Oil)
- {
- var oilName = ToNull(GetString(item, "OIL_NM"));
- if (oilName is null)
- {
- continue;
- }
- rows.Add(new CommodityRow(
- commodityMarket,
- oilName,
- oilName,
- tradeDate.Value,
- ParseDecimal(GetString(item, "WT_AVG_PRC")),
- 0m,
- 0m,
- 0m,
- null,
- null,
- ParseNullableDecimal(GetString(item, "WT_DIS_AVG_PRC")),
- ParseLong(GetString(item, "ACC_TRDVOL")),
- ParseLong(GetString(item, "ACC_TRDVAL"))
- ));
- }
- else
- {
- var code = ToNull(GetString(item, "ISU_CD"));
- var name = GetString(item, "ISU_NM");
- if (code is null || name.Length == 0)
- {
- continue;
- }
- rows.Add(new CommodityRow(
- commodityMarket,
- code,
- name,
- tradeDate.Value,
- ParseDecimal(GetString(item, "TDD_CLSPRC")),
- ParseDecimal(GetString(item, "TDD_OPNPRC")),
- ParseDecimal(GetString(item, "TDD_HGPRC")),
- ParseDecimal(GetString(item, "TDD_LWPRC")),
- ParseNullableDecimal(GetString(item, "CMPPREVDD_PRC")),
- ParseNullableDecimal(GetString(item, "FLUC_RT")),
- null,
- 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
- };
- }
- /// <summary>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
- private static string? ToNull(string raw)
- {
- return raw.Length == 0 || raw == "-" ? null : raw;
- }
- 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;
- }
- /// <summary>가격 문자열 → decimal 그대로. "" 나 "-" 등 파싱 실패는 0.</summary>
- private static decimal ParseDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
- }
- /// <summary>전일대비/등락률/협의가격 — "" 나 "-" 등 파싱 실패는 null (0 과 구분, 미제공은 값 없음)</summary>
- private static decimal? ParseNullableDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
- }
- }
|