using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class KrxFuturesParserTests { [TestMethod] public void ParseDaily_General_MapsFields_WithSettleAndOpenInterest() { // KRX OpenAPI(fut_bydd_trd) 실검증 샘플 — 정산가(SETL_PRC)·현물가(SPOT_PRC)·미결제약정(ACC_OPNINT_QTY), 8자리 영숫자 코드 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"10년국채 선물","MKT_NM":"정규","ISU_CD":"167W3000","ISU_NM":"10년국채 F 202503 (주간)","TDD_CLSPRC":"117.67","CMPPREVDD_PRC":"0.75","TDD_OPNPRC":"117.10","TDD_HGPRC":"117.71","TDD_LWPRC":"116.82","SPOT_PRC":"117.76","SETL_PRC":"117.67","ACC_TRDVOL":"107306","ACC_TRDVAL":"12592876260000","ACC_OPNINT_QTY":"215549"} ]} """; var rows = KrxFuturesParser.ParseDaily(json, FuturesKind.General); Assert.AreEqual(1, rows.Count); var fut = rows[0]; Assert.AreEqual(FuturesKind.General, fut.FuturesKind, "상품군은 호출 인자로 결정"); Assert.AreEqual("167W3000", fut.IsuCode, "8자리 영숫자 코드 원문 유지"); Assert.AreEqual("10년국채 F 202503 (주간)", fut.IsuName); Assert.AreEqual(new DateOnly(2025, 1, 2), fut.TradeDate); Assert.AreEqual(117.67m, fut.Close); Assert.AreEqual(117.10m, fut.Open); Assert.AreEqual(117.71m, fut.High); Assert.AreEqual(116.82m, fut.Low); Assert.AreEqual(0.75m, fut.ChangeAmount); Assert.AreEqual(117.76m, fut.SpotPrice, "현물가 (SPOT_PRC)"); Assert.AreEqual(117.67m, fut.SettlePrice, "정산가 (SETL_PRC)"); Assert.AreEqual(107306L, fut.Volume); Assert.AreEqual(12592876260000L, fut.TradeValue); Assert.AreEqual(215549L, fut.OpenInterest, "미결제약정 (ACC_OPNINT_QTY)"); Assert.AreEqual("10년국채 선물", fut.ProductName); Assert.AreEqual("정규", fut.MarketName); } [TestMethod] public void ParseDaily_StockKospi_And_StockKosdaq_KindFromArgument() { // 주식선물(유가) eqsfu_stk_bydd_trd 샘플 — kind 는 호출 인자로 결정 const string kospiJson = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"BGF리테일 선물","MKT_NM":"정규","ISU_CD":"1FRW1000","ISU_NM":"BGF리테일 F 202501 ","TDD_CLSPRC":"101800.00","CMPPREVDD_PRC":"-1300.00","TDD_OPNPRC":"102000.00","TDD_HGPRC":"103500.00","TDD_LWPRC":"100800.00","SPOT_PRC":"101500.00","SETL_PRC":"101800.00","ACC_TRDVOL":"908","ACC_TRDVAL":"924753000","ACC_OPNINT_QTY":"3172"} ]} """; var kospi = KrxFuturesParser.ParseDaily(kospiJson, FuturesKind.StockKospi); Assert.AreEqual(1, kospi.Count); Assert.AreEqual(FuturesKind.StockKospi, kospi[0].FuturesKind); Assert.AreEqual(101800.00m, kospi[0].Close); Assert.AreEqual(-1300.00m, kospi[0].ChangeAmount, "음수 대비"); Assert.AreEqual(3172L, kospi[0].OpenInterest); // 코스닥 선물 eqkfu_ksq_bydd_trd 샘플 const string ksqJson = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"CJ ENM 선물","MKT_NM":"정규","ISU_CD":"1DTW1000","ISU_NM":"CJ ENM F 202501 ","TDD_CLSPRC":"53300.00","CMPPREVDD_PRC":"-700.00","TDD_OPNPRC":"54300.00","TDD_HGPRC":"54700.00","TDD_LWPRC":"53000.00","SPOT_PRC":"53700.00","SETL_PRC":"53300.00","ACC_TRDVOL":"2945","ACC_TRDVAL":"1572553000","ACC_OPNINT_QTY":"6906"} ]} """; var ksq = KrxFuturesParser.ParseDaily(ksqJson, FuturesKind.StockKosdaq); Assert.AreEqual(1, ksq.Count); Assert.AreEqual(FuturesKind.StockKosdaq, ksq[0].FuturesKind); Assert.AreEqual("1DTW1000", ksq[0].IsuCode); Assert.AreEqual(6906L, ksq[0].OpenInterest); } [TestMethod] public void ParseDaily_NoTrade_EmptyPricesZero_MissingSettleAndOpenInterestNull() { // 미체결 종목 — 종가/시고저/대비 "" → 0, SETL_PRC "" → null, ACC_OPNINT_QTY "" → null (스프레드 종목) const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"10년국채 선물","MKT_NM":"정규","ISU_CD":"467W3W6S","ISU_NM":"10년국채 SP 2503-2506 (주간)","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","SPOT_PRC":"117.76","SETL_PRC":"","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":""} ]} """; var rows = KrxFuturesParser.ParseDaily(json, FuturesKind.General); Assert.AreEqual(1, rows.Count); var fut = rows[0]; Assert.AreEqual(0m, fut.Close, "미체결 종가는 0"); Assert.AreEqual(0m, fut.Open); Assert.AreEqual(0m, fut.ChangeAmount, "빈 대비는 0"); Assert.AreEqual(117.76m, fut.SpotPrice, "현물가는 채워짐"); Assert.IsNull(fut.SettlePrice, "\"\" 정산가는 null"); Assert.IsNull(fut.OpenInterest, "\"\" 미결제약정은 null (0 과 구분)"); } [TestMethod] public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty() { // basDd 미반영 — OutBlock_1 이 빈 배열 Assert.AreEqual(0, KrxFuturesParser.ParseDaily("""{"OutBlock_1":[]}""", FuturesKind.General).Count); // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건 Assert.AreEqual(0, KrxFuturesParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", FuturesKind.StockKospi).Count); } }