KrxOptionsParserTests.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. using Domain.Entities.Stocks.ValueObject;
  2. using Infrastructure.StockData;
  3. namespace Application.Tests;
  4. [TestClass]
  5. public sealed class KrxOptionsParserTests
  6. {
  7. [TestMethod]
  8. public void ParseDaily_General_MapsFields_CallPut_StrikeFromName_OpenInterest()
  9. {
  10. // KRX OpenAPI(opt_bydd_trd) 실검증 샘플 — RGHT_TP_NM(CALL/PUT), IMP_VOLT(내재변동성), NXTDD_BAS_PRC(익일정산가), ACC_OPNINT_QTY(미결제약정)
  11. // 행사가는 필드 없이 ISU_NM "미니코스피 C 202501 232.5 (정규)" 에서 파싱 → 232.5
  12. const string json = """
  13. {"OutBlock_1":[
  14. {"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"},
  15. {"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"}
  16. ]}
  17. """;
  18. var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.General);
  19. Assert.AreEqual(2, rows.Count);
  20. var call = rows[0];
  21. Assert.AreEqual(OptionsKind.General, call.OptionsKind, "상품군은 호출 인자로 결정");
  22. Assert.AreEqual("205W1232", call.IsuCode, "8자리 영숫자 코드 원문 유지");
  23. Assert.AreEqual(OptionRightType.Call, call.RightType, "RGHT_TP_NM CALL → Call");
  24. Assert.AreEqual(232.5m, call.StrikePrice, "행사가는 ISU_NM 에서 파싱 (232.5)");
  25. Assert.AreEqual(0m, call.Close, "미체결 종가는 0");
  26. Assert.AreEqual(3.00m, call.ImpliedVolatility, "내재변동성 (IMP_VOLT)");
  27. Assert.AreEqual(85.40m, call.NextDayBasePrice, "익일정산가 (NXTDD_BAS_PRC)");
  28. Assert.AreEqual(3L, call.OpenInterest, "미결제약정 (ACC_OPNINT_QTY)");
  29. Assert.AreEqual("미니코스피200 옵션", call.ProductName);
  30. var put = rows[1];
  31. Assert.AreEqual(OptionRightType.Put, put.RightType, "RGHT_TP_NM PUT → Put");
  32. Assert.AreEqual(232.5m, put.StrikePrice);
  33. Assert.AreEqual(1.25m, put.Close);
  34. Assert.AreEqual(-0.10m, put.ChangeAmount, "음수 대비");
  35. Assert.AreEqual(1234L, put.Volume, "거래량 콤마 제거");
  36. Assert.AreEqual(56700000L, put.TradeValue, "거래대금 콤마 제거");
  37. Assert.AreEqual(7890L, put.OpenInterest, "미결제약정 콤마 제거");
  38. }
  39. [TestMethod]
  40. public void ParseDaily_StockOption_StrikeWithThousandsSeparator_IgnoresMultiplier()
  41. {
  42. // 주식옵션(유가) eqsop_bydd_trd 샘플 — 행사가에 천단위 콤마 + 뒤에 승수 "( 10)" 표기. "150,000" → 150000, 승수는 무시
  43. const string json = """
  44. {"OutBlock_1":[
  45. {"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"}
  46. ]}
  47. """;
  48. var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.StockKospi);
  49. Assert.AreEqual(1, rows.Count);
  50. var opt = rows[0];
  51. Assert.AreEqual(OptionsKind.StockKospi, opt.OptionsKind);
  52. Assert.AreEqual(OptionRightType.Call, opt.RightType);
  53. Assert.AreEqual(150000m, opt.StrikePrice, "행사가 콤마 제거, 승수 (10) 무시 → 150000");
  54. Assert.AreEqual(38.30m, opt.ImpliedVolatility);
  55. Assert.AreEqual(0L, opt.OpenInterest, "\"0\" 미결제약정은 0 (null 아님)");
  56. }
  57. [TestMethod]
  58. public void ParseDaily_KosdaqOption_StrikeParsed_KindFromArgument()
  59. {
  60. // 주식옵션(코스닥) eqkop_bydd_trd 샘플 — "씨젠 C 202501 15,500( 10)" → 15500
  61. const string json = """
  62. {"OutBlock_1":[
  63. {"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"}
  64. ]}
  65. """;
  66. var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.StockKosdaq);
  67. Assert.AreEqual(1, rows.Count);
  68. Assert.AreEqual(OptionsKind.StockKosdaq, rows[0].OptionsKind);
  69. Assert.AreEqual(15500m, rows[0].StrikePrice, "행사가 15,500 → 15500");
  70. }
  71. [TestMethod]
  72. public void ParseDaily_NullsAndUnparsableStrike_AreHandled()
  73. {
  74. // IMP_VOLT/NXTDD_BAS_PRC = "-" → null, 행사가 파싱 불가한 종목명 → StrikePrice null, ACC_OPNINT_QTY "-" → null
  75. const string json = """
  76. {"OutBlock_1":[
  77. {"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":"-"}
  78. ]}
  79. """;
  80. var rows = KrxOptionsParser.ParseDaily(json, OptionsKind.General);
  81. Assert.AreEqual(1, rows.Count);
  82. var opt = rows[0];
  83. Assert.AreEqual(OptionRightType.Put, opt.RightType);
  84. Assert.IsNull(opt.StrikePrice, "행사가 패턴 없는 종목명 → null");
  85. Assert.IsNull(opt.ImpliedVolatility, "\"-\" 내재변동성은 null");
  86. Assert.IsNull(opt.NextDayBasePrice, "\"-\" 익일정산가는 null");
  87. Assert.IsNull(opt.OpenInterest, "\"-\" 미결제약정은 null");
  88. }
  89. [TestMethod]
  90. public void ParseDaily_UnknownRightType_RowSkipped()
  91. {
  92. // RGHT_TP_NM 이 CALL/PUT 이 아니면(빈값 등) 해당 행 skip
  93. const string json = """
  94. {"OutBlock_1":[
  95. {"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"}
  96. ]}
  97. """;
  98. Assert.AreEqual(0, KrxOptionsParser.ParseDaily(json, OptionsKind.General).Count, "권리유형 미상 행은 skip");
  99. }
  100. [TestMethod]
  101. public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty()
  102. {
  103. Assert.AreEqual(0, KrxOptionsParser.ParseDaily("""{"OutBlock_1":[]}""", OptionsKind.General).Count);
  104. Assert.AreEqual(0, KrxOptionsParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", OptionsKind.StockKospi).Count);
  105. }
  106. }