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;
}
}