KrxEtpParserTests.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. using Domain.Entities.Stocks.ValueObject;
  2. using Infrastructure.StockData;
  3. namespace Application.Tests;
  4. [TestClass]
  5. public sealed class KrxEtpParserTests
  6. {
  7. [TestMethod]
  8. public void ParseDaily_Etf_MapsFields_FromSampleJson()
  9. {
  10. // KRX OpenAPI(etf_bydd_trd) 실검증 샘플 — ISU_CD 6자리 숫자, NAV/순자산총액/기초지수, FLUC_RT 는 소수 % 그대로
  11. const string json = """
  12. {"OutBlock_1":[
  13. {"BAS_DD":"20250102","ISU_CD":"451060","ISU_NM":"1Q K200액티브","TDD_CLSPRC":"10,175","CMPPREVDD_PRC":"5","FLUC_RT":"0.05","NAV":"10184.36","TDD_OPNPRC":"10,120","TDD_HGPRC":"10,230","TDD_LWPRC":"10,090","ACC_TRDVOL":"39,706","ACC_TRDVAL":"402,743,695","MKTCAP":"97,680,000,000","INVSTASST_NETASST_TOTAMT":"97,769,860,763","LIST_SHRS":"9,600,000","IDX_IND_NM":"코스피 200","OBJ_STKPRC_IDX":"317.77","CMPPREVDD_IDX":"-0.05","FLUC_RT_IDX":"-0.02"},
  14. {"BAS_DD":"20250102","ISU_CD":"466400","ISU_NM":"1Q 25-08 회사채(A+이상)액티브","TDD_CLSPRC":"107075","CMPPREVDD_PRC":"60","FLUC_RT":"0.06","NAV":"107050.99","TDD_OPNPRC":"107075","TDD_HGPRC":"107075","TDD_LWPRC":"107055","ACC_TRDVOL":"461","ACC_TRDVAL":"49358415","MKTCAP":"113178275000","INVSTASST_NETASST_TOTAMT":"113152900578","LIST_SHRS":"1057000","IDX_IND_NM":"KIS 2025-08만기형 크레딧 A+이상 지수(총수익)","OBJ_STKPRC_IDX":"","CMPPREVDD_IDX":"","FLUC_RT_IDX":""}
  15. ]}
  16. """;
  17. var rows = KrxEtpParser.ParseDaily(json, EtpType.ETF);
  18. Assert.AreEqual(2, rows.Count);
  19. var etf = rows[0];
  20. Assert.AreEqual(EtpType.ETF, etf.EtpType, "유형은 호출 인자로 결정");
  21. Assert.AreEqual("451060", etf.Code);
  22. Assert.AreEqual("1Q K200액티브", etf.Name);
  23. Assert.AreEqual(new DateOnly(2025, 1, 2), etf.TradeDate);
  24. Assert.AreEqual(10175m, etf.Close, "종가 콤마 제거");
  25. Assert.AreEqual(10120m, etf.Open);
  26. Assert.AreEqual(10230m, etf.High);
  27. Assert.AreEqual(10090m, etf.Low);
  28. Assert.AreEqual(5m, etf.ChangeAmount);
  29. Assert.AreEqual(0.05m, etf.ChangeRate, "FLUC_RT 는 소수 % 그대로 (KrxStockParser 와 동일 스케일, Bp 아님)");
  30. Assert.AreEqual(39706L, etf.Volume, "거래량 콤마 제거");
  31. Assert.AreEqual(402743695L, etf.TradeValue, "거래대금 콤마 제거");
  32. Assert.AreEqual(97680000000L, etf.MarketCap);
  33. Assert.AreEqual(9600000L, etf.ListedShares);
  34. Assert.AreEqual(10184.36m, etf.Nav, "ETF NAV 소수 파싱");
  35. Assert.AreEqual(97769860763L, etf.NetAssetTotal, "ETF 순자산총액(INVSTASST_NETASST_TOTAMT)");
  36. Assert.AreEqual("코스피 200", etf.BaseIndexName);
  37. Assert.AreEqual(317.77m, etf.BaseIndexClose);
  38. Assert.IsNull(etf.Underlying, "ETF 는 기초자산 없음");
  39. Assert.IsNull(etf.UnderlyingClose);
  40. // 두 번째 행: OBJ_STKPRC_IDX 가 "" → BaseIndexClose null
  41. Assert.IsNull(rows[1].BaseIndexClose, "빈 기초지수 종가는 null");
  42. }
  43. [TestMethod]
  44. public void ParseDaily_Etn_MapsFields_UsesIndicatorValueAsNav()
  45. {
  46. // KRX OpenAPI(etn_bydd_trd) 실검증 샘플 — PER1SECU_INDIC_VAL(지표가치 IV) → Nav, INDIC_VAL_AMT → NetAssetTotal
  47. const string json = """
  48. {"OutBlock_1":[
  49. {"BAS_DD":"20250102","ISU_CD":"580048","ISU_NM":"KB CSI 300 ETN","TDD_CLSPRC":"10,275","CMPPREVDD_PRC":"-350","FLUC_RT":"-3.29","PER1SECU_INDIC_VAL":"10102.02","TDD_OPNPRC":"10,410","TDD_HGPRC":"10,410","TDD_LWPRC":"10,255","ACC_TRDVOL":"39,246","ACC_TRDVAL":"405,862,805","MKTCAP":"20,550,000,000","INDIC_VAL_AMT":"20,204,040,000","LIST_SHRS":"2,000,000","IDX_IND_NM":"CSI 300 NTR","OBJ_STKPRC_IDX":"5,294.90","CMPPREVDD_IDX":"-247.61","FLUC_RT_IDX":"-4.47"}
  50. ]}
  51. """;
  52. var rows = KrxEtpParser.ParseDaily(json, EtpType.ETN);
  53. Assert.AreEqual(1, rows.Count);
  54. var etn = rows[0];
  55. Assert.AreEqual(EtpType.ETN, etn.EtpType);
  56. Assert.AreEqual("580048", etn.Code);
  57. Assert.AreEqual(10275m, etn.Close);
  58. Assert.AreEqual(-3.29m, etn.ChangeRate, "ETN FLUC_RT 는 소수 % 그대로");
  59. Assert.AreEqual(10102.02m, etn.Nav, "ETN 은 PER1SECU_INDIC_VAL(IV) 을 Nav 로 매핑");
  60. Assert.AreEqual(20204040000L, etn.NetAssetTotal, "ETN 지표가치총액(INDIC_VAL_AMT)");
  61. Assert.AreEqual("CSI 300 NTR", etn.BaseIndexName);
  62. Assert.AreEqual(5294.90m, etn.BaseIndexClose, "기초지수 종가 콤마 제거");
  63. Assert.IsNull(etn.Underlying, "ETN 은 기초자산 없음");
  64. }
  65. [TestMethod]
  66. public void ParseDaily_Elw_AlphanumericCode_NoOwnChangeRate_MapsUnderlying()
  67. {
  68. // KRX OpenAPI(elw_bydd_trd) 실검증 샘플 — ISU_CD 영숫자 6자리(58K001), 자체 등락률 없음(ChangeRate=0), ULY_* 기초자산
  69. const string json = """
  70. {"OutBlock_1":[
  71. {"BAS_DD":"20250102","ISU_CD":"58K001","ISU_NM":"KBK001삼성전자콜","TDD_CLSPRC":"5","CMPPREVDD_PRC":"0","TDD_OPNPRC":"0","TDD_HGPRC":"0","TDD_LWPRC":"0","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"55,000,000","LIST_SHRS":"11,000,000","ULY_NM":"삼성전자","ULY_PRC":"53,400","CMPPREVDD_PRC_ULY":"200","FLUC_RT_ULY":"0.38"}
  72. ]}
  73. """;
  74. var rows = KrxEtpParser.ParseDaily(json, EtpType.ELW);
  75. Assert.AreEqual(1, rows.Count);
  76. var elw = rows[0];
  77. Assert.AreEqual(EtpType.ELW, elw.EtpType);
  78. Assert.AreEqual("58K001", elw.Code, "ELW 영숫자 코드는 그대로 유지 (숫자 전용 아님)");
  79. Assert.AreEqual("KBK001삼성전자콜", elw.Name);
  80. Assert.AreEqual(5m, elw.Close);
  81. Assert.AreEqual(0m, elw.ChangeRate, "ELW 는 자체 등락률 필드가 없어 0");
  82. Assert.AreEqual(55000000L, elw.MarketCap, "시가총액 콤마 제거");
  83. Assert.AreEqual(11000000L, elw.ListedShares);
  84. Assert.AreEqual("삼성전자", elw.Underlying, "ULY_NM → Underlying");
  85. Assert.AreEqual(53400m, elw.UnderlyingClose, "ULY_PRC 콤마 제거");
  86. Assert.IsNull(elw.Nav, "ELW 는 NAV 없음");
  87. Assert.IsNull(elw.NetAssetTotal);
  88. Assert.IsNull(elw.BaseIndexName, "ELW 는 기초지수 없음");
  89. }
  90. [TestMethod]
  91. public void ParseDaily_NullsAndMissingValues_AreHandled()
  92. {
  93. // MKTCAP/LIST_SHRS = "-" → null, NAV = "-" → null
  94. const string json = """
  95. {"OutBlock_1":[
  96. {"BAS_DD":"20250102","ISU_CD":"580049","ISU_NM":"KB CSI 500 ETN","TDD_CLSPRC":"9345","CMPPREVDD_PRC":"-585","FLUC_RT":"-5.89","PER1SECU_INDIC_VAL":"-","TDD_OPNPRC":"0","TDD_HGPRC":"0","TDD_LWPRC":"0","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"-","INDIC_VAL_AMT":"-","LIST_SHRS":"-","IDX_IND_NM":"CSI 500 NTR","OBJ_STKPRC_IDX":"-","CMPPREVDD_IDX":"-","FLUC_RT_IDX":"-"}
  97. ]}
  98. """;
  99. var rows = KrxEtpParser.ParseDaily(json, EtpType.ETN);
  100. Assert.AreEqual(1, rows.Count);
  101. var etn = rows[0];
  102. Assert.IsNull(etn.MarketCap, "\"-\" 시가총액은 null");
  103. Assert.IsNull(etn.ListedShares, "\"-\" 상장증권수는 null");
  104. Assert.IsNull(etn.Nav, "\"-\" 지표가치는 null");
  105. Assert.IsNull(etn.NetAssetTotal, "\"-\" 지표가치총액은 null");
  106. Assert.IsNull(etn.BaseIndexClose, "\"-\" 기초지수 종가는 null");
  107. Assert.AreEqual(-5.89m, etn.ChangeRate, "FLUC_RT 소수 % 그대로");
  108. }
  109. [TestMethod]
  110. public void ParseDaily_EmptyOrMissingBlock_ReturnsEmpty()
  111. {
  112. // basDd 미반영 — OutBlock_1 이 빈 배열
  113. Assert.AreEqual(0, KrxEtpParser.ParseDaily("""{"OutBlock_1":[]}""", EtpType.ETF).Count);
  114. // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건
  115. Assert.AreEqual(0, KrxEtpParser.ParseDaily("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", EtpType.ELW).Count);
  116. }
  117. }