| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- 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");
- }
- }
|