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": [ { ...string values }, ... ] }. 모든 값은 문자열, 천단위 콤마 가능, 결측치는 "" 또는 "-".
/// 선물은 옵션과 달리 정산가(SETL_PRC)·현물가(SPOT_PRC)·미결제약정(ACC_OPNINT_QTY)을 갖는다:
/// - 가격: TDD_CLSPRC/OPNPRC/HGPRC/LWPRC (미체결 시 "") + CMPPREVDD_PRC(대비)
/// - 선물 고유: PROD_NM(상품구분), MKT_NM(정규/야간), SPOT_PRC(현물가 nullable), SETL_PRC(정산가 nullable — 스프레드 종목 미제공), ACC_OPNINT_QTY(미결제약정 nullable)
/// 종목코드(ISU_CD)는 8자리 영숫자(예: 167W3000) — 6자리 정규화 없이 원문 유지. FuturesKind 는 호출 엔드포인트로 결정.
/// 일반선물(fut_bydd_trd)은 동일 ISU_CD 로 정규·야간 세션 행이 각각 오므로 MKT_NM("정규"/"야간") → Session 으로 매핑한다
/// (야간 행은 종목명에도 "(야간)" 표기 — MKT_NM 부재/그 외 값은 종목명 fallback 후 Regular).
///
public static class KrxFuturesParser
{
/// 선물 일별매매 1행 (미체결가·정산가·미결제약정은 nullable)
public sealed record FuturesRow(
FuturesKind FuturesKind,
string IsuCode,
string IsuName,
DateOnly TradeDate,
MarketSession Session,
decimal Close,
decimal Open,
decimal High,
decimal Low,
decimal ChangeAmount,
decimal? SpotPrice,
decimal? SettlePrice,
long Volume,
long TradeValue,
long? OpenInterest,
string? ProductName,
string? MarketName
);
/// fut/eqsfu/eqkfu_bydd_trd 파싱. futuresKind 는 호출 엔드포인트로 결정.
public static IReadOnlyList ParseDaily(string json, FuturesKind futuresKind)
{
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 isuCode = ToNull(GetString(item, "ISU_CD"));
var isuName = GetString(item, "ISU_NM");
var tradeDate = ParseDate(GetString(item, "BAS_DD"));
if (isuCode is null || isuName.Length == 0 || tradeDate is null)
{
continue;
}
rows.Add(new FuturesRow(
futuresKind,
isuCode,
isuName,
tradeDate.Value,
MapSession(GetString(item, "MKT_NM"), isuName),
ParseDecimal(GetString(item, "TDD_CLSPRC")),
ParseDecimal(GetString(item, "TDD_OPNPRC")),
ParseDecimal(GetString(item, "TDD_HGPRC")),
ParseDecimal(GetString(item, "TDD_LWPRC")),
ParseDecimal(GetString(item, "CMPPREVDD_PRC")),
ParseNullableDecimal(GetString(item, "SPOT_PRC")),
ParseNullableDecimal(GetString(item, "SETL_PRC")),
ParseLong(GetString(item, "ACC_TRDVOL")),
ParseLong(GetString(item, "ACC_TRDVAL")),
ParseNullableLong(GetString(item, "ACC_OPNINT_QTY")),
ToNull(GetString(item, "PROD_NM")),
ToNull(GetString(item, "MKT_NM"))
));
}
return rows;
}
/// MKT_NM("정규"/"야간") → Session. "야간" 표기(MKT_NM 또는 종목명)만 Night, 그 외/부재는 Regular.
private static MarketSession MapSession(string marketName, string isuName)
{
if (marketName.Contains("야간", StringComparison.Ordinal) || isuName.Contains("야간", StringComparison.Ordinal))
{
return MarketSession.Night;
}
return MarketSession.Regular;
}
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
};
}
/// 빈 문자열/"-" 은 null, 나머지는 trim 값
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;
}
private static long? ParseNullableLong(string raw)
{
return long.TryParse(raw, NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
}
/// 가격/대비 문자열 → decimal 그대로. "" 나 "-" 등 파싱 실패는 0.
private static decimal ParseDecimal(string raw)
{
return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0m;
}
/// 현물가/정산가 등 — "" 나 "-" 등 파싱 실패는 null (0 과 구분, 미제공은 값 없음)
private static decimal? ParseNullableDecimal(string raw)
{
return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
}
}