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 }, ... ] }. 모든 값은 문자열, 천단위 콤마 포함 가능, 결측치는 "" 또는 "-".
/// 등락률(FLUC_RT)은 소수 % 그대로 저장한다 (WarrantDailyTrade.ChangeRate 가 decimal % — KrxStockParser 와 동일 스케일, Bp 아님).
/// 유형별 필드 차이(라이브 검증):
/// - 증권(SubscriptionWarrant, sw_bydd_trd): EXER_PRC(행사가), EXST_STRT_DD/EXST_END_DD(행사기간)
/// - 증서(SubscriptionRight, sr_bydd_trd): ISU_PRC(발행가), DELIST_DD(상장폐지일)
/// 공통 대상주식: TARSTK_ISU_SRT_CD/TARSTK_ISU_NM/TARSTK_ISU_PRSNT_PRC.
/// 단축코드(ISU_CD)는 8자리 영숫자(예: 1099621D) — 6자리 정규화 없이 원문 유지. 시장은 MKT_NM 기준.
///
public static class KrxWarrantParser
{
/// 신주인수권 일별매매 1행 (등락률은 소수 % 그대로 — Bp 아님)
public sealed record WarrantRow(
WarrantType WarrantType,
string Code,
string Name,
StockMarket Market,
DateOnly TradeDate,
decimal Close,
decimal Open,
decimal High,
decimal Low,
decimal ChangeAmount,
decimal ChangeRate,
long Volume,
long TradeValue,
long? MarketCap,
long? ListedShares,
string? TargetStockCode,
string? TargetStockName,
decimal? TargetStockPrice,
decimal? ExercisePrice,
DateOnly? ExerciseStartDate,
DateOnly? ExerciseEndDate,
decimal? IssuePrice,
DateOnly? DelistDate
);
/// sw/sr_bydd_trd 파싱. warrantType 은 호출 엔드포인트로 결정, 유형별 필드는 조건 분기.
public static IReadOnlyList ParseDaily(string json, WarrantType warrantType)
{
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 code = ToNull(GetString(item, "ISU_CD"));
var name = GetString(item, "ISU_NM");
var tradeDate = ParseDate(GetString(item, "BAS_DD"));
if (code is null || name.Length == 0 || tradeDate is null)
{
continue;
}
decimal? exercisePrice = null;
DateOnly? exerciseStartDate = null;
DateOnly? exerciseEndDate = null;
decimal? issuePrice = null;
DateOnly? delistDate = null;
switch (warrantType)
{
case WarrantType.SubscriptionWarrant:
exercisePrice = ParseNullableDecimal(GetString(item, "EXER_PRC"));
exerciseStartDate = ParseDate(GetString(item, "EXST_STRT_DD"));
exerciseEndDate = ParseDate(GetString(item, "EXST_END_DD"));
break;
case WarrantType.SubscriptionRight:
issuePrice = ParseNullableDecimal(GetString(item, "ISU_PRC"));
delistDate = ParseDate(GetString(item, "DELIST_DD"));
break;
}
rows.Add(new WarrantRow(
warrantType,
code,
name,
MapMarket(GetString(item, "MKT_NM")),
tradeDate.Value,
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")),
ParseDecimal(GetString(item, "FLUC_RT")),
ParseLong(GetString(item, "ACC_TRDVOL")),
ParseLong(GetString(item, "ACC_TRDVAL")),
ParseNullableLong(GetString(item, "MKTCAP")),
ParseNullableLong(GetString(item, "LIST_SHRS")),
ToNull(GetString(item, "TARSTK_ISU_SRT_CD")),
ToNull(GetString(item, "TARSTK_ISU_NM")),
ParseNullableDecimal(GetString(item, "TARSTK_ISU_PRSNT_PRC")),
exercisePrice,
exerciseStartDate,
exerciseEndDate,
issuePrice,
delistDate
));
}
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
};
}
/// MKT_NM("KOSPI"/"KOSDAQ"/"KONEX") → StockMarket. 미상은 KOSPI 로 폴백.
private static StockMarket MapMarket(string raw)
{
return raw switch
{
"KOSDAQ" => StockMarket.KOSDAQ,
"KONEX" => StockMarket.KONEX,
_ => StockMarket.KOSPI
};
}
/// 빈 문자열/"-" 은 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;
}
}