using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class KrxOptionsParserTests { [TestMethod] public void ParseDaily_General_MapsFields_CallPut_StrikeFromName_OpenInterest() { // KRX OpenAPI(opt_bydd_trd) 실검증 샘플 — RGHT_TP_NM(CALL/PUT), IMP_VOLT(내재변동성), NXTDD_BAS_PRC(익일정산가), ACC_OPNINT_QTY(미결제약정) // 행사가는 필드 없이 ISU_NM "미니코스피 C 202501 232.5 (정규)" 에서 파싱 → 232.5 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"미니코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"205W1232","ISU_NM":"미니코스피 C 202501 232.5 (정규)","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","IMP_VOLT":"3.00","NXTDD_BAS_PRC":"85.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"3"}, {"BAS_DD":"20250102","PROD_NM":"미니코스피200 옵션","RGHT_TP_NM":"PUT","ISU_CD":"305W1232","ISU_NM":"미니코스피 P 202501 232.5 (정규)","TDD_CLSPRC":"1.25","CMPPREVDD_PRC":"-0.10","TDD_OPNPRC":"1.30","TDD_HGPRC":"1.40","TDD_LWPRC":"1.20","IMP_VOLT":"12.34","NXTDD_BAS_PRC":"1.25","ACC_TRDVOL":"1,234","ACC_TRDVAL":"56,700,000","ACC_OPNINT_QTY":"7,890"} ]} """; var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.General); Assert.AreEqual(2, rows.Count); var call = rows[0]; Assert.AreEqual(OptionsKind.General, call.OptionsKind, "상품군은 호출 인자로 결정"); Assert.AreEqual("205W1232", call.IsuCode, "8자리 영숫자 코드 원문 유지"); Assert.AreEqual(OptionRightType.Call, call.RightType, "RGHT_TP_NM CALL → Call"); Assert.AreEqual(232.5m, call.StrikePrice, "행사가는 ISU_NM 에서 파싱 (232.5)"); Assert.AreEqual(0m, call.Close, "미체결 종가는 0"); Assert.AreEqual(3.00m, call.ImpliedVolatility, "내재변동성 (IMP_VOLT)"); Assert.AreEqual(85.40m, call.NextDayBasePrice, "익일정산가 (NXTDD_BAS_PRC)"); Assert.AreEqual(3L, call.OpenInterest, "미결제약정 (ACC_OPNINT_QTY)"); Assert.AreEqual("미니코스피200 옵션", call.ProductName); var put = rows[1]; Assert.AreEqual(OptionRightType.Put, put.RightType, "RGHT_TP_NM PUT → Put"); Assert.AreEqual(232.5m, put.StrikePrice); Assert.AreEqual(1.25m, put.Close); Assert.AreEqual(-0.10m, put.ChangeAmount, "음수 대비"); Assert.AreEqual(1234L, put.Volume, "거래량 콤마 제거"); Assert.AreEqual(56700000L, put.TradeValue, "거래대금 콤마 제거"); Assert.AreEqual(7890L, put.OpenInterest, "미결제약정 콤마 제거"); } [TestMethod] public void ParseDaily_StockOption_StrikeWithThousandsSeparator_IgnoresMultiplier() { // 주식옵션(유가) eqsop_bydd_trd 샘플 — 행사가에 천단위 콤마 + 뒤에 승수 "( 10)" 표기. "150,000" → 150000, 승수는 무시 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"HD한국조선해양 옵션","RGHT_TP_NM":"CALL","ISU_CD":"239W1041","ISU_NM":"HD한국조선 C 202501 150,000( 10)","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","IMP_VOLT":"38.30","NXTDD_BAS_PRC":"81000.00","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"} ]} """; var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.StockKospi); Assert.AreEqual(1, rows.Count); var opt = rows[0]; Assert.AreEqual(OptionsKind.StockKospi, opt.OptionsKind); Assert.AreEqual(OptionRightType.Call, opt.RightType); Assert.AreEqual(150000m, opt.StrikePrice, "행사가 콤마 제거, 승수 (10) 무시 → 150000"); Assert.AreEqual(38.30m, opt.ImpliedVolatility); Assert.AreEqual(0L, opt.OpenInterest, "\"0\" 미결제약정은 0 (null 아님)"); } [TestMethod] public void ParseDaily_KosdaqOption_StrikeParsed_KindFromArgument() { // 주식옵션(코스닥) eqkop_bydd_trd 샘플 — "씨젠 C 202501 15,500( 10)" → 15500 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"씨젠 옵션","RGHT_TP_NM":"CALL","ISU_CD":"2CLW1062","ISU_NM":"씨젠 C 202501 15,500( 10)","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","IMP_VOLT":"48.70","NXTDD_BAS_PRC":"8700.00","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"} ]} """; var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.StockKosdaq); Assert.AreEqual(1, rows.Count); Assert.AreEqual(OptionsKind.StockKosdaq, rows[0].OptionsKind); Assert.AreEqual(15500m, rows[0].StrikePrice, "행사가 15,500 → 15500"); } [TestMethod] public void ParseDaily_NullsAndUnparsableStrike_AreHandled() { // IMP_VOLT/NXTDD_BAS_PRC = "-" → null, 행사가 파싱 불가한 종목명 → StrikePrice null, ACC_OPNINT_QTY "-" → null const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"PUT","ISU_CD":"305W9999","ISU_NM":"코스피200 위클리물","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","IMP_VOLT":"-","NXTDD_BAS_PRC":"-","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"-"} ]} """; var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.General); Assert.AreEqual(1, rows.Count); var opt = rows[0]; Assert.AreEqual(OptionRightType.Put, opt.RightType); Assert.IsNull(opt.StrikePrice, "행사가 패턴 없는 종목명 → null"); Assert.IsNull(opt.ImpliedVolatility, "\"-\" 내재변동성은 null"); Assert.IsNull(opt.NextDayBasePrice, "\"-\" 익일정산가는 null"); Assert.IsNull(opt.OpenInterest, "\"-\" 미결제약정은 null"); } [TestMethod] public void ParseDaily_UnknownRightType_RowSkipped() { // RGHT_TP_NM 이 CALL/PUT 이 아니면(빈값 등) 해당 행 skip const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"","ISU_CD":"205W1232","ISU_NM":"코스피 C 202501 300.0","TDD_CLSPRC":"1.0","CMPPREVDD_PRC":"0","TDD_OPNPRC":"1.0","TDD_HGPRC":"1.0","TDD_LWPRC":"1.0","IMP_VOLT":"3.0","NXTDD_BAS_PRC":"1.0","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"} ]} """; Assert.AreEqual(0, KrxOptionsParser.ParseDaily(json, OptionsKind.General).Count, "권리유형 미상 행은 skip"); } [TestMethod] public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty() { Assert.AreEqual(0, KrxOptionsParser.ParseDaily("""{"OutBlock_1":[]}""", OptionsKind.General).Count); Assert.AreEqual(0, KrxOptionsParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", OptionsKind.StockKospi).Count); } [TestMethod] public void ParseDaily_SessionMapping_FromIsuName_NoMktNmField() { // 일반옵션(opt_bydd_trd)은 MKT_NM 필드 없이 동일 ISU_CD 로 정규·야간 행이 각각 온다 (라이브 확인: 18,908행 = 9,454 코드 × 2). // 세션 표기는 종목명 "(정규)"/"(야간)" → Regular/Night, 표기 없는 위클리 변형은 Regular (설계된 degrade) const string json = """ {"OutBlock_1":[ {"BAS_DD":"20260706","PROD_NM":"미니코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"205WA232","ISU_NM":"미니코스피 C 202607 232.5 (정규)","TDD_CLSPRC":"1.50","CMPPREVDD_PRC":"0.10","TDD_OPNPRC":"1.40","TDD_HGPRC":"1.55","TDD_LWPRC":"1.38","IMP_VOLT":"12.00","NXTDD_BAS_PRC":"1.50","ACC_TRDVOL":"1,000","ACC_TRDVAL":"150,000,000","ACC_OPNINT_QTY":"500"}, {"BAS_DD":"20260706","PROD_NM":"미니코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"205WA232","ISU_NM":"미니코스피 C 202607 232.5 (야간)","TDD_CLSPRC":"1.45","CMPPREVDD_PRC":"-0.05","TDD_OPNPRC":"1.50","TDD_HGPRC":"1.50","TDD_LWPRC":"1.44","IMP_VOLT":"12.10","NXTDD_BAS_PRC":"1.45","ACC_TRDVOL":"10","ACC_TRDVAL":"1,500,000","ACC_OPNINT_QTY":"500"}, {"BAS_DD":"20260706","PROD_NM":"코스피200 위클리 옵션","RGHT_TP_NM":"PUT","ISU_CD":"BAK36002","ISU_NM":"코스피200위클리(목) P 1,325","TDD_CLSPRC":"","CMPPREVDD_PRC":"","TDD_OPNPRC":"","TDD_HGPRC":"","TDD_LWPRC":"","IMP_VOLT":"-","NXTDD_BAS_PRC":"-","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"} ]} """; var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.General); Assert.AreEqual(3, rows.Count); Assert.AreEqual(MarketSession.Regular, rows[0].Session, "종목명 (정규) → Regular"); Assert.AreEqual(MarketSession.Night, rows[1].Session, "종목명 (야간) → Night (MKT_NM 없이도 판정)"); Assert.AreEqual(rows[0].IsuCode, rows[1].IsuCode, "동일 종목코드가 세션별 별도 행으로 유지"); Assert.AreEqual(1.50m, rows[0].Close, "정규 종가 보존"); Assert.AreEqual(1.45m, rows[1].Close, "야간 종가 보존"); Assert.AreEqual(MarketSession.Regular, rows[2].Session, "세션 표기 없는 위클리 변형 → Regular"); } }