using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class KrxBondParserTests { [TestMethod] public void ParseDaily_KtsGovt_MapsPriceYieldAndGovBondFields() { // KRX OpenAPI(kts_bydd_trd) 실검증 샘플 — ISU_CD 12자리 ISIN, 가격 CLSPRC/OPNPRC/HGPRC/LWPRC, 수익률 *_YD, 국채전문 전용 BND_EXP_TP_NM/GOVBND_ISU_TP_NM const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","MKT_NM":"국채전문유통시장","ISU_CD":"KR103503GE96","ISU_NM":"국고02875-2609(24-9)","BND_EXP_TP_NM":"2","GOVBND_ISU_TP_NM":"지표","CLSPRC":"10122.5","CMPPREVDD_PRC":"14.5","CLSPRC_YD":"2.681","OPNPRC":"10110.5","OPNPRC_YD":"2.754","HGPRC":"10123.0","HGPRC_YD":"2.678","LWPRC":"10110.5","LWPRC_YD":"2.754","ACC_TRDVOL":"70000000000","ACC_TRDVAL":"70834100000"} ]} """; var rows = KrxBondParser.ParseDaily(json, BondMarket.KtsGovt); Assert.AreEqual(1, rows.Count); var b = rows[0]; Assert.AreEqual(BondMarket.KtsGovt, b.BondMarket, "시장은 호출 인자로 결정"); Assert.AreEqual("KR103503GE96", b.Code, "12자리 ISIN 원문 유지 (6자리 정규화 안 함)"); Assert.AreEqual("국고02875-2609(24-9)", b.Name); Assert.AreEqual(new DateOnly(2025, 1, 2), b.TradeDate); Assert.AreEqual(10122.5m, b.Close, "CLSPRC 소수 파싱"); Assert.AreEqual(10110.5m, b.Open); Assert.AreEqual(10123.0m, b.High); Assert.AreEqual(10110.5m, b.Low); Assert.AreEqual(14.5m, b.ChangeAmount, "CMPPREVDD_PRC"); Assert.AreEqual(2.681m, b.YieldToMaturity, "CLSPRC_YD = 만기수익률(종가수익률)"); Assert.AreEqual(2.754m, b.OpenYield); Assert.AreEqual(2.678m, b.HighYield); Assert.AreEqual(2.754m, b.LowYield); Assert.AreEqual(70000000000L, b.Volume); Assert.AreEqual(70834100000L, b.TradeValue); Assert.AreEqual("2", b.MaturityYears, "국채전문 BND_EXP_TP_NM"); Assert.AreEqual("지표", b.IssueType, "국채전문 GOVBND_ISU_TP_NM"); } [TestMethod] public void ParseDaily_General_EmptyChangeAmount_IsZero_NoGovBondFields() { // KRX OpenAPI(bnd_bydd_trd) 실검증 샘플 — CMPPREVDD_PRC 빈값("") → 0, 국채전문 전용 필드 없음 → null const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","MKT_NM":"일반채권시장","ISU_CD":"KR101501DAB5","ISU_NM":"국민주택1종20-11","CLSPRC":"10230.0","CMPPREVDD_PRC":"","CLSPRC_YD":"3.009","OPNPRC":"10230.0","OPNPRC_YD":"3.009","HGPRC":"10230.0","HGPRC_YD":"3.009","LWPRC":"10230.0","LWPRC_YD":"3.009","ACC_TRDVOL":"29000","ACC_TRDVAL":"29667"} ]} """; var rows = KrxBondParser.ParseDaily(json, BondMarket.General); Assert.AreEqual(1, rows.Count); var b = rows[0]; Assert.AreEqual(BondMarket.General, b.BondMarket); Assert.AreEqual("KR101501DAB5", b.Code); Assert.AreEqual(10230.0m, b.Close); Assert.AreEqual(0m, b.ChangeAmount, "빈 CMPPREVDD_PRC 는 0"); Assert.AreEqual(3.009m, b.YieldToMaturity); Assert.AreEqual(29000L, b.Volume); Assert.AreEqual(29667L, b.TradeValue); Assert.IsNull(b.MaturityYears, "일반채권은 국채전문 전용 필드 없음"); Assert.IsNull(b.IssueType); } [TestMethod] public void ParseDaily_Small_MapsFields_WithThousandsCommas() { // KRX OpenAPI(smb_bydd_trd) 실검증 샘플 — 소액채권, 천단위 콤마 포함 방어 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","MKT_NM":"소액채권시장","ISU_CD":"KR101501DEC5","ISU_NM":"국민주택1종24-12","CLSPRC":"9,253.0","CMPPREVDD_PRC":"79.0","CLSPRC_YD":"2.888","OPNPRC":"9,221.0","OPNPRC_YD":"2.959","HGPRC":"9,264.0","HGPRC_YD":"2.863","LWPRC":"9,221.0","LWPRC_YD":"2.959","ACC_TRDVOL":"117,311,623,000","ACC_TRDVAL":"108,262,446,570"} ]} """; var rows = KrxBondParser.ParseDaily(json, BondMarket.Small); Assert.AreEqual(1, rows.Count); var b = rows[0]; Assert.AreEqual(BondMarket.Small, b.BondMarket); Assert.AreEqual("KR101501DEC5", b.Code); Assert.AreEqual(9253.0m, b.Close, "종가 콤마 제거"); Assert.AreEqual(79.0m, b.ChangeAmount); Assert.AreEqual(2.888m, b.YieldToMaturity); Assert.AreEqual(117311623000L, b.Volume, "거래량 콤마 제거"); Assert.AreEqual(108262446570L, b.TradeValue, "거래대금 콤마 제거"); Assert.IsNull(b.MaturityYears, "소액채권은 국채전문 전용 필드 없음"); Assert.IsNull(b.IssueType); } [TestMethod] public void ParseDaily_NullYields_AreHandled() { // 미체결 채권 — 수익률(*_YD) 빈값("")/"-" → null (0 과 구분). KtsGovt 전용 필드가 빈값이어도 null. const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","MKT_NM":"국채전문유통시장","ISU_CD":"KR103502GEC4","ISU_NM":"국고03000-3412(24-13)","BND_EXP_TP_NM":"","GOVBND_ISU_TP_NM":"-","CLSPRC":"10231.0","CMPPREVDD_PRC":"-","CLSPRC_YD":"","OPNPRC":"0","OPNPRC_YD":"-","HGPRC":"0","HGPRC_YD":"","LWPRC":"0","LWPRC_YD":"","ACC_TRDVOL":"0","ACC_TRDVAL":"0"} ]} """; var rows = KrxBondParser.ParseDaily(json, BondMarket.KtsGovt); Assert.AreEqual(1, rows.Count); var b = rows[0]; Assert.IsNull(b.YieldToMaturity, "\"\" 만기수익률은 null"); Assert.IsNull(b.OpenYield, "\"-\" 시가수익률은 null"); Assert.IsNull(b.HighYield); Assert.IsNull(b.LowYield); Assert.AreEqual(0m, b.ChangeAmount, "\"-\" 전일대비는 0"); Assert.IsNull(b.MaturityYears, "빈 BND_EXP_TP_NM 은 null"); Assert.IsNull(b.IssueType, "\"-\" GOVBND_ISU_TP_NM 은 null"); } [TestMethod] public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty() { // basDd 미반영 — OutBlock_1 이 빈 배열 Assert.AreEqual(0, KrxBondParser.ParseDaily("""{"OutBlock_1":[]}""", BondMarket.KtsGovt).Count); // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건 Assert.AreEqual(0, KrxBondParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", BondMarket.General).Count); } }