using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class KrxEtpParserTests { [TestMethod] public void ParseDaily_Etf_MapsFields_FromSampleJson() { // KRX OpenAPI(etf_bydd_trd) 실검증 샘플 — ISU_CD 6자리 숫자, NAV/순자산총액/기초지수, FLUC_RT 는 소수 % 그대로 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"451060","ISU_NM":"1Q K200액티브","TDD_CLSPRC":"10,175","CMPPREVDD_PRC":"5","FLUC_RT":"0.05","NAV":"10184.36","TDD_OPNPRC":"10,120","TDD_HGPRC":"10,230","TDD_LWPRC":"10,090","ACC_TRDVOL":"39,706","ACC_TRDVAL":"402,743,695","MKTCAP":"97,680,000,000","INVSTASST_NETASST_TOTAMT":"97,769,860,763","LIST_SHRS":"9,600,000","IDX_IND_NM":"코스피 200","OBJ_STKPRC_IDX":"317.77","CMPPREVDD_IDX":"-0.05","FLUC_RT_IDX":"-0.02"}, {"BAS_DD":"20250102","ISU_CD":"466400","ISU_NM":"1Q 25-08 회사채(A+이상)액티브","TDD_CLSPRC":"107075","CMPPREVDD_PRC":"60","FLUC_RT":"0.06","NAV":"107050.99","TDD_OPNPRC":"107075","TDD_HGPRC":"107075","TDD_LWPRC":"107055","ACC_TRDVOL":"461","ACC_TRDVAL":"49358415","MKTCAP":"113178275000","INVSTASST_NETASST_TOTAMT":"113152900578","LIST_SHRS":"1057000","IDX_IND_NM":"KIS 2025-08만기형 크레딧 A+이상 지수(총수익)","OBJ_STKPRC_IDX":"","CMPPREVDD_IDX":"","FLUC_RT_IDX":""} ]} """; var rows = KrxEtpParser.ParseDaily(json, EtpType.ETF); Assert.AreEqual(2, rows.Count); var etf = rows[0]; Assert.AreEqual(EtpType.ETF, etf.EtpType, "유형은 호출 인자로 결정"); Assert.AreEqual("451060", etf.Code); Assert.AreEqual("1Q K200액티브", etf.Name); Assert.AreEqual(new DateOnly(2025, 1, 2), etf.TradeDate); Assert.AreEqual(10175m, etf.Close, "종가 콤마 제거"); Assert.AreEqual(10120m, etf.Open); Assert.AreEqual(10230m, etf.High); Assert.AreEqual(10090m, etf.Low); Assert.AreEqual(5m, etf.ChangeAmount); Assert.AreEqual(0.05m, etf.ChangeRate, "FLUC_RT 는 소수 % 그대로 (KrxStockParser 와 동일 스케일, Bp 아님)"); Assert.AreEqual(39706L, etf.Volume, "거래량 콤마 제거"); Assert.AreEqual(402743695L, etf.TradeValue, "거래대금 콤마 제거"); Assert.AreEqual(97680000000L, etf.MarketCap); Assert.AreEqual(9600000L, etf.ListedShares); Assert.AreEqual(10184.36m, etf.Nav, "ETF NAV 소수 파싱"); Assert.AreEqual(97769860763L, etf.NetAssetTotal, "ETF 순자산총액(INVSTASST_NETASST_TOTAMT)"); Assert.AreEqual("코스피 200", etf.BaseIndexName); Assert.AreEqual(317.77m, etf.BaseIndexClose); Assert.IsNull(etf.Underlying, "ETF 는 기초자산 없음"); Assert.IsNull(etf.UnderlyingClose); // 두 번째 행: OBJ_STKPRC_IDX 가 "" → BaseIndexClose null Assert.IsNull(rows[1].BaseIndexClose, "빈 기초지수 종가는 null"); } [TestMethod] public void ParseDaily_Etn_MapsFields_UsesIndicatorValueAsNav() { // KRX OpenAPI(etn_bydd_trd) 실검증 샘플 — PER1SECU_INDIC_VAL(지표가치 IV) → Nav, INDIC_VAL_AMT → NetAssetTotal const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"580048","ISU_NM":"KB CSI 300 ETN","TDD_CLSPRC":"10,275","CMPPREVDD_PRC":"-350","FLUC_RT":"-3.29","PER1SECU_INDIC_VAL":"10102.02","TDD_OPNPRC":"10,410","TDD_HGPRC":"10,410","TDD_LWPRC":"10,255","ACC_TRDVOL":"39,246","ACC_TRDVAL":"405,862,805","MKTCAP":"20,550,000,000","INDIC_VAL_AMT":"20,204,040,000","LIST_SHRS":"2,000,000","IDX_IND_NM":"CSI 300 NTR","OBJ_STKPRC_IDX":"5,294.90","CMPPREVDD_IDX":"-247.61","FLUC_RT_IDX":"-4.47"} ]} """; var rows = KrxEtpParser.ParseDaily(json, EtpType.ETN); Assert.AreEqual(1, rows.Count); var etn = rows[0]; Assert.AreEqual(EtpType.ETN, etn.EtpType); Assert.AreEqual("580048", etn.Code); Assert.AreEqual(10275m, etn.Close); Assert.AreEqual(-3.29m, etn.ChangeRate, "ETN FLUC_RT 는 소수 % 그대로"); Assert.AreEqual(10102.02m, etn.Nav, "ETN 은 PER1SECU_INDIC_VAL(IV) 을 Nav 로 매핑"); Assert.AreEqual(20204040000L, etn.NetAssetTotal, "ETN 지표가치총액(INDIC_VAL_AMT)"); Assert.AreEqual("CSI 300 NTR", etn.BaseIndexName); Assert.AreEqual(5294.90m, etn.BaseIndexClose, "기초지수 종가 콤마 제거"); Assert.IsNull(etn.Underlying, "ETN 은 기초자산 없음"); } [TestMethod] public void ParseDaily_Elw_AlphanumericCode_NoOwnChangeRate_MapsUnderlying() { // KRX OpenAPI(elw_bydd_trd) 실검증 샘플 — ISU_CD 영숫자 6자리(58K001), 자체 등락률 없음(ChangeRate=0), ULY_* 기초자산 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"58K001","ISU_NM":"KBK001삼성전자콜","TDD_CLSPRC":"5","CMPPREVDD_PRC":"0","TDD_OPNPRC":"0","TDD_HGPRC":"0","TDD_LWPRC":"0","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"55,000,000","LIST_SHRS":"11,000,000","ULY_NM":"삼성전자","ULY_PRC":"53,400","CMPPREVDD_PRC_ULY":"200","FLUC_RT_ULY":"0.38"} ]} """; var rows = KrxEtpParser.ParseDaily(json, EtpType.ELW); Assert.AreEqual(1, rows.Count); var elw = rows[0]; Assert.AreEqual(EtpType.ELW, elw.EtpType); Assert.AreEqual("58K001", elw.Code, "ELW 영숫자 코드는 그대로 유지 (숫자 전용 아님)"); Assert.AreEqual("KBK001삼성전자콜", elw.Name); Assert.AreEqual(5m, elw.Close); Assert.AreEqual(0m, elw.ChangeRate, "ELW 는 자체 등락률 필드가 없어 0"); Assert.AreEqual(55000000L, elw.MarketCap, "시가총액 콤마 제거"); Assert.AreEqual(11000000L, elw.ListedShares); Assert.AreEqual("삼성전자", elw.Underlying, "ULY_NM → Underlying"); Assert.AreEqual(53400m, elw.UnderlyingClose, "ULY_PRC 콤마 제거"); Assert.IsNull(elw.Nav, "ELW 는 NAV 없음"); Assert.IsNull(elw.NetAssetTotal); Assert.IsNull(elw.BaseIndexName, "ELW 는 기초지수 없음"); } [TestMethod] public void ParseDaily_NullsAndMissingValues_AreHandled() { // MKTCAP/LIST_SHRS = "-" → null, NAV = "-" → null const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"580049","ISU_NM":"KB CSI 500 ETN","TDD_CLSPRC":"9345","CMPPREVDD_PRC":"-585","FLUC_RT":"-5.89","PER1SECU_INDIC_VAL":"-","TDD_OPNPRC":"0","TDD_HGPRC":"0","TDD_LWPRC":"0","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"-","INDIC_VAL_AMT":"-","LIST_SHRS":"-","IDX_IND_NM":"CSI 500 NTR","OBJ_STKPRC_IDX":"-","CMPPREVDD_IDX":"-","FLUC_RT_IDX":"-"} ]} """; var rows = KrxEtpParser.ParseDaily(json, EtpType.ETN); Assert.AreEqual(1, rows.Count); var etn = rows[0]; Assert.IsNull(etn.MarketCap, "\"-\" 시가총액은 null"); Assert.IsNull(etn.ListedShares, "\"-\" 상장증권수는 null"); Assert.IsNull(etn.Nav, "\"-\" 지표가치는 null"); Assert.IsNull(etn.NetAssetTotal, "\"-\" 지표가치총액은 null"); Assert.IsNull(etn.BaseIndexClose, "\"-\" 기초지수 종가는 null"); Assert.AreEqual(-5.89m, etn.ChangeRate, "FLUC_RT 소수 % 그대로"); } [TestMethod] public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty() { // basDd 미반영 — OutBlock_1 이 빈 배열 Assert.AreEqual(0, KrxEtpParser.ParseDaily("""{"OutBlock_1":[]}""", EtpType.ETF).Count); // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건 Assert.AreEqual(0, KrxEtpParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", EtpType.ELW).Count); } }