KrxBondParserTests.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. using Domain.Entities.Stocks.ValueObject;
  2. using Infrastructure.StockData;
  3. namespace Application.Tests;
  4. [TestClass]
  5. public sealed class KrxBondParserTests
  6. {
  7. [TestMethod]
  8. public void ParseDaily_KtsGovt_MapsPriceYieldAndGovBondFields()
  9. {
  10. // KRX OpenAPI(kts_bydd_trd) 실검증 샘플 — ISU_CD 12자리 ISIN, 가격 CLSPRC/OPNPRC/HGPRC/LWPRC, 수익률 *_YD, 국채전문 전용 BND_EXP_TP_NM/GOVBND_ISU_TP_NM
  11. const string json = """
  12. {"OutBlock_1":[
  13. {"BAS_DD":"20250102","MKT_NM":"국채전문유통시장","ISU_CD":"KR103503GE96","ISU_NM":"국고02875-2609(24-9)","BND_EXP_TP_NM":"2","GOVBND_ISU_TP_NM":"지표","CLSPRC":"10122.5","CMPPREVDD_PRC":"14.5","CLSPRC_YD":"2.681","OPNPRC":"10110.5","OPNPRC_YD":"2.754","HGPRC":"10123.0","HGPRC_YD":"2.678","LWPRC":"10110.5","LWPRC_YD":"2.754","ACC_TRDVOL":"70000000000","ACC_TRDVAL":"70834100000"}
  14. ]}
  15. """;
  16. var rows = KrxBondParser.ParseDaily(json, BondMarket.KtsGovt);
  17. Assert.AreEqual(1, rows.Count);
  18. var b = rows[0];
  19. Assert.AreEqual(BondMarket.KtsGovt, b.BondMarket, "시장은 호출 인자로 결정");
  20. Assert.AreEqual("KR103503GE96", b.Code, "12자리 ISIN 원문 유지 (6자리 정규화 안 함)");
  21. Assert.AreEqual("국고02875-2609(24-9)", b.Name);
  22. Assert.AreEqual(new DateOnly(2025, 1, 2), b.TradeDate);
  23. Assert.AreEqual(10122.5m, b.Close, "CLSPRC 소수 파싱");
  24. Assert.AreEqual(10110.5m, b.Open);
  25. Assert.AreEqual(10123.0m, b.High);
  26. Assert.AreEqual(10110.5m, b.Low);
  27. Assert.AreEqual(14.5m, b.ChangeAmount, "CMPPREVDD_PRC");
  28. Assert.AreEqual(2.681m, b.YieldToMaturity, "CLSPRC_YD = 만기수익률(종가수익률)");
  29. Assert.AreEqual(2.754m, b.OpenYield);
  30. Assert.AreEqual(2.678m, b.HighYield);
  31. Assert.AreEqual(2.754m, b.LowYield);
  32. Assert.AreEqual(70000000000L, b.Volume);
  33. Assert.AreEqual(70834100000L, b.TradeValue);
  34. Assert.AreEqual("2", b.MaturityYears, "국채전문 BND_EXP_TP_NM");
  35. Assert.AreEqual("지표", b.IssueType, "국채전문 GOVBND_ISU_TP_NM");
  36. }
  37. [TestMethod]
  38. public void ParseDaily_General_EmptyChangeAmount_IsZero_NoGovBondFields()
  39. {
  40. // KRX OpenAPI(bnd_bydd_trd) 실검증 샘플 — CMPPREVDD_PRC 빈값("") → 0, 국채전문 전용 필드 없음 → null
  41. const string json = """
  42. {"OutBlock_1":[
  43. {"BAS_DD":"20250102","MKT_NM":"일반채권시장","ISU_CD":"KR101501DAB5","ISU_NM":"국민주택1종20-11","CLSPRC":"10230.0","CMPPREVDD_PRC":"","CLSPRC_YD":"3.009","OPNPRC":"10230.0","OPNPRC_YD":"3.009","HGPRC":"10230.0","HGPRC_YD":"3.009","LWPRC":"10230.0","LWPRC_YD":"3.009","ACC_TRDVOL":"29000","ACC_TRDVAL":"29667"}
  44. ]}
  45. """;
  46. var rows = KrxBondParser.ParseDaily(json, BondMarket.General);
  47. Assert.AreEqual(1, rows.Count);
  48. var b = rows[0];
  49. Assert.AreEqual(BondMarket.General, b.BondMarket);
  50. Assert.AreEqual("KR101501DAB5", b.Code);
  51. Assert.AreEqual(10230.0m, b.Close);
  52. Assert.AreEqual(0m, b.ChangeAmount, "빈 CMPPREVDD_PRC 는 0");
  53. Assert.AreEqual(3.009m, b.YieldToMaturity);
  54. Assert.AreEqual(29000L, b.Volume);
  55. Assert.AreEqual(29667L, b.TradeValue);
  56. Assert.IsNull(b.MaturityYears, "일반채권은 국채전문 전용 필드 없음");
  57. Assert.IsNull(b.IssueType);
  58. }
  59. [TestMethod]
  60. public void ParseDaily_Small_MapsFields_WithThousandsCommas()
  61. {
  62. // KRX OpenAPI(smb_bydd_trd) 실검증 샘플 — 소액채권, 천단위 콤마 포함 방어
  63. const string json = """
  64. {"OutBlock_1":[
  65. {"BAS_DD":"20250102","MKT_NM":"소액채권시장","ISU_CD":"KR101501DEC5","ISU_NM":"국민주택1종24-12","CLSPRC":"9,253.0","CMPPREVDD_PRC":"79.0","CLSPRC_YD":"2.888","OPNPRC":"9,221.0","OPNPRC_YD":"2.959","HGPRC":"9,264.0","HGPRC_YD":"2.863","LWPRC":"9,221.0","LWPRC_YD":"2.959","ACC_TRDVOL":"117,311,623,000","ACC_TRDVAL":"108,262,446,570"}
  66. ]}
  67. """;
  68. var rows = KrxBondParser.ParseDaily(json, BondMarket.Small);
  69. Assert.AreEqual(1, rows.Count);
  70. var b = rows[0];
  71. Assert.AreEqual(BondMarket.Small, b.BondMarket);
  72. Assert.AreEqual("KR101501DEC5", b.Code);
  73. Assert.AreEqual(9253.0m, b.Close, "종가 콤마 제거");
  74. Assert.AreEqual(79.0m, b.ChangeAmount);
  75. Assert.AreEqual(2.888m, b.YieldToMaturity);
  76. Assert.AreEqual(117311623000L, b.Volume, "거래량 콤마 제거");
  77. Assert.AreEqual(108262446570L, b.TradeValue, "거래대금 콤마 제거");
  78. Assert.IsNull(b.MaturityYears, "소액채권은 국채전문 전용 필드 없음");
  79. Assert.IsNull(b.IssueType);
  80. }
  81. [TestMethod]
  82. public void ParseDaily_NullYields_AreHandled()
  83. {
  84. // 미체결 채권 — 수익률(*_YD) 빈값("")/"-" → null (0 과 구분). KtsGovt 전용 필드가 빈값이어도 null.
  85. const string json = """
  86. {"OutBlock_1":[
  87. {"BAS_DD":"20250102","MKT_NM":"국채전문유통시장","ISU_CD":"KR103502GEC4","ISU_NM":"국고03000-3412(24-13)","BND_EXP_TP_NM":"","GOVBND_ISU_TP_NM":"-","CLSPRC":"10231.0","CMPPREVDD_PRC":"-","CLSPRC_YD":"","OPNPRC":"0","OPNPRC_YD":"-","HGPRC":"0","HGPRC_YD":"","LWPRC":"0","LWPRC_YD":"","ACC_TRDVOL":"0","ACC_TRDVAL":"0"}
  88. ]}
  89. """;
  90. var rows = KrxBondParser.ParseDaily(json, BondMarket.KtsGovt);
  91. Assert.AreEqual(1, rows.Count);
  92. var b = rows[0];
  93. Assert.IsNull(b.YieldToMaturity, "\"\" 만기수익률은 null");
  94. Assert.IsNull(b.OpenYield, "\"-\" 시가수익률은 null");
  95. Assert.IsNull(b.HighYield);
  96. Assert.IsNull(b.LowYield);
  97. Assert.AreEqual(0m, b.ChangeAmount, "\"-\" 전일대비는 0");
  98. Assert.IsNull(b.MaturityYears, "빈 BND_EXP_TP_NM 은 null");
  99. Assert.IsNull(b.IssueType, "\"-\" GOVBND_ISU_TP_NM 은 null");
  100. }
  101. [TestMethod]
  102. public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty()
  103. {
  104. // basDd 미반영 — OutBlock_1 이 빈 배열
  105. Assert.AreEqual(0, KrxBondParser.ParseDaily("""{"OutBlock_1":[]}""", BondMarket.KtsGovt).Count);
  106. // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건
  107. Assert.AreEqual(0, KrxBondParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", BondMarket.General).Count);
  108. }
  109. }