using System.Globalization;
using System.Text.Json;
namespace Infrastructure.StockData;
///
/// KRX OpenAPI(data-dbg.krx.co.kr) 사회책임투자채권 정보(esg/sri_bond_info) 응답 파서 — 순수 C# (System.Text.Json).
/// 응답 골격: { "OutBlock_1": [ { BAS_DD, ISUR_NM, ISU_CD, SRI_BND_TP_NM, ISU_NM, LIST_DD, ISU_DD, REDMPT_DD, ISU_RT, ISU_AMT, LIST_AMT, BND_TP_NM }, ... ] }.
/// 가격/거래량이 아니라 발행 정보 스냅샷이다. 날짜(LIST_DD/ISU_DD/REDMPT_DD)는 yyyyMMdd, 미제공/오류 시 null. 금액은 원 단위 long, 표면이자율(ISU_RT)은 decimal %.
/// 채권 일별매매(bon)와 shape 이 완전히 달라 별도 파서. 표준코드(ISU_CD)는 12자리 ISIN 원문 유지.
///
public static class KrxSriBondParser
{
/// 사회책임투자채권 정보 1행 (날짜/표면이자율은 결측 시 null)
public sealed record SriBondRow(
string Code,
DateOnly TradeDate,
string IssuerName,
string SriBondType,
string Name,
DateOnly? ListDate,
DateOnly? IssueDate,
DateOnly? RedemptionDate,
decimal? CouponRate,
long IssueAmount,
long ListAmount,
string? BondType
);
public static IReadOnlyList ParseDaily(string json)
{
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 issuerName = GetString(item, "ISUR_NM");
var name = GetString(item, "ISU_NM");
var tradeDate = ParseDate(GetString(item, "BAS_DD"));
if (code is null || issuerName.Length == 0 || name.Length == 0 || tradeDate is null)
{
continue;
}
rows.Add(new SriBondRow(
code,
tradeDate.Value,
issuerName,
GetString(item, "SRI_BND_TP_NM"),
name,
ParseNullableDate(GetString(item, "LIST_DD")),
ParseNullableDate(GetString(item, "ISU_DD")),
ParseNullableDate(GetString(item, "REDMPT_DD")),
ParseNullableDecimal(GetString(item, "ISU_RT")),
ParseLong(GetString(item, "ISU_AMT")),
ParseLong(GetString(item, "LIST_AMT")),
ToNull(GetString(item, "BND_TP_NM"))
));
}
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
};
}
/// 빈 문자열/"-" 은 null, 나머지는 trim 값
private static string? ToNull(string raw)
{
return raw.Length == 0 || raw == "-" ? null : raw;
}
/// 기준일자(BAS_DD) — 필수. 파싱 실패는 null(행 제외).
private static DateOnly? ParseDate(string raw)
{
return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
}
/// 상장일/발행일/상환일 — yyyyMMdd. 빈값/오류는 null.
private static DateOnly? ParseNullableDate(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;
}
/// 표면이자율(%) — "" 나 "-" 등 파싱 실패는 null (0% 과 구분)
private static decimal? ParseNullableDecimal(string raw)
{
return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
}
}