KrxStockParserTests.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. using Domain.Entities.Stocks.ValueObject;
  2. using Infrastructure.StockData;
  3. namespace Application.Tests;
  4. [TestClass]
  5. public sealed class KrxStockParserTests
  6. {
  7. [TestMethod]
  8. public void ParseDailyPrices_MapsFields_FromSampleJson()
  9. {
  10. // KRX OpenAPI(stk_bydd_trd) 표준 응답 샘플 — ISU_CD=6자리 단축코드, FLUC_RT 는 소수 % 그대로 저장(Bp 아님)
  11. const string json = """
  12. {"OutBlock_1":[
  13. {"BAS_DD":"20250102","ISU_CD":"005930","ISU_NM":"삼성전자","MKT_NM":"KOSPI","SECT_TP_NM":"","TDD_CLSPRC":"70,500","CMPPREVDD_PRC":"-500","FLUC_RT":"-0.70","TDD_OPNPRC":"71,000","TDD_HGPRC":"71,200","TDD_LWPRC":"70,300","ACC_TRDVOL":"12,345,678","ACC_TRDVAL":"870,000,000,000","MKTCAP":"420,870,169,275,000","LIST_SHRS":"5,969,782,550"},
  14. {"BAS_DD":"20250102","ISU_CD":"000660","ISU_NM":"SK하이닉스","MKT_NM":"KOSPI","SECT_TP_NM":"","TDD_CLSPRC":"210000","CMPPREVDD_PRC":"3000","FLUC_RT":"1.45","TDD_OPNPRC":"208000","TDD_HGPRC":"211500","TDD_LWPRC":"207000","ACC_TRDVOL":"2345678","ACC_TRDVAL":"490000000000","MKTCAP":"-","LIST_SHRS":"-"}
  15. ]}
  16. """;
  17. var rows = KrxStockParser.ParseDailyPrices(json, StockMarket.KOSPI);
  18. Assert.AreEqual(2, rows.Count);
  19. var samsung = rows[0];
  20. Assert.AreEqual("005930", samsung.Code);
  21. Assert.AreEqual(new DateOnly(2025, 1, 2), samsung.TradingDate);
  22. Assert.AreEqual(71000, samsung.Open, "콤마 천단위를 제거하고 시가 파싱");
  23. Assert.AreEqual(71200, samsung.High);
  24. Assert.AreEqual(70300, samsung.Low);
  25. Assert.AreEqual(70500, samsung.Close);
  26. Assert.AreEqual(12345678L, samsung.Volume, "거래량 콤마 제거");
  27. Assert.AreEqual(870000000000L, samsung.TradingValue, "거래대금 콤마 제거");
  28. Assert.AreEqual(-500, samsung.ChangeAmount);
  29. Assert.AreEqual(-0.70m, samsung.ChangeRate, "FLUC_RT 는 소수 % 그대로 (DataGoKr fltRt 와 동일 스케일, Bp 아님)");
  30. Assert.AreEqual(420870169275000L, samsung.MarketCap);
  31. Assert.AreEqual(5969782550L, samsung.ListedShares);
  32. var hynix = rows[1];
  33. Assert.AreEqual("000660", hynix.Code);
  34. Assert.AreEqual(1.45m, hynix.ChangeRate);
  35. Assert.IsNull(hynix.MarketCap, "\"-\" 시가총액은 null");
  36. Assert.IsNull(hynix.ListedShares, "\"-\" 상장주식수는 null");
  37. }
  38. [TestMethod]
  39. public void ParseDailyPrices_NormalizesPrefixedCode()
  40. {
  41. // 'A' 프리픽스 단축코드는 뒤 6자리로 정규화
  42. const string json = """
  43. {"OutBlock_1":[
  44. {"BAS_DD":"20250102","ISU_CD":"A000660","ISU_NM":"SK하이닉스","MKT_NM":"KOSPI","TDD_CLSPRC":"210000","CMPPREVDD_PRC":"0","FLUC_RT":"0.00","TDD_OPNPRC":"210000","TDD_HGPRC":"210000","TDD_LWPRC":"210000","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"0","LIST_SHRS":"0"}
  45. ]}
  46. """;
  47. var rows = KrxStockParser.ParseDailyPrices(json, StockMarket.KOSPI);
  48. Assert.AreEqual(1, rows.Count);
  49. Assert.AreEqual("000660", rows[0].Code, "'A' 프리픽스 단축코드는 뒤 6자리로 정규화한다");
  50. }
  51. [TestMethod]
  52. public void ParseDailyPrices_EmptyOrMissingBlock_ReturnsEmpty()
  53. {
  54. // basDd 미반영 — OutBlock_1 이 빈 배열
  55. Assert.AreEqual(0, KrxStockParser.ParseDailyPrices("""{"OutBlock_1":[]}""", StockMarket.KOSDAQ).Count);
  56. // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건
  57. Assert.AreEqual(0, KrxStockParser.ParseDailyPrices("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", StockMarket.KOSPI).Count);
  58. }
  59. [TestMethod]
  60. public void ParseMasterInfo_MapsFields_KospiAndKosdaq()
  61. {
  62. // KRX OpenAPI(stk_isu_base_info/ksq_isu_base_info) 표준 응답 샘플 — ISU_SRT_CD=단축코드, ISU_CD=표준코드 ISIN
  63. const string kospiJson = """
  64. {"OutBlock_1":[
  65. {"ISU_CD":"KR7005930003","ISU_SRT_CD":"005930","ISU_NM":"삼성전자보통주","ISU_ABBRV":"삼성전자","ISU_ENG_NM":"SAMSUNG ELECTRONICS CO.,LTD.","LIST_DD":"19750611","MKT_TP_NM":"KOSPI","SECUGRP_NM":"주권","SECT_TP_NM":"","KIND_STKCERT_TP_NM":"보통주","PARVAL":"100","LIST_SHRS":"5969782550"}
  66. ]}
  67. """;
  68. var kospi = KrxStockParser.ParseMasterInfo(kospiJson, StockMarket.KOSPI);
  69. Assert.AreEqual(1, kospi.Count);
  70. var samsung = kospi[0];
  71. Assert.AreEqual("005930", samsung.Code, "ISU_SRT_CD → Code");
  72. Assert.AreEqual("KR7005930003", samsung.Isin, "ISU_CD(표준코드) → ISIN");
  73. Assert.AreEqual("삼성전자", samsung.Name, "ISU_ABBRV(약칭) → Name");
  74. Assert.AreEqual("SAMSUNG ELECTRONICS CO.,LTD.", samsung.EnglishName);
  75. Assert.AreEqual(StockMarket.KOSPI, samsung.Market, "market 은 호출 엔드포인트 인자로 결정");
  76. Assert.IsNull(samsung.SectorName, "빈 SECT_TP_NM 은 null");
  77. Assert.AreEqual(new DateOnly(1975, 6, 11), samsung.ListedDate, "LIST_DD → ListedDate");
  78. // KOSDAQ 은 SECT_TP_NM(소속부) 이 채워져 온다
  79. const string kosdaqJson = """
  80. {"OutBlock_1":[
  81. {"ISU_CD":"KR7058820002","ISU_SRT_CD":"058820","ISU_NM":"CMG제약","ISU_ABBRV":"CMG제약","ISU_ENG_NM":"CMG Pharmaceutical Co., Ltd.","LIST_DD":"20010831","MKT_TP_NM":"KOSDAQ","SECUGRP_NM":"주권","SECT_TP_NM":"우량기업부","KIND_STKCERT_TP_NM":"보통주","PARVAL":"500","LIST_SHRS":"111111111"}
  82. ]}
  83. """;
  84. var kosdaq = KrxStockParser.ParseMasterInfo(kosdaqJson, StockMarket.KOSDAQ);
  85. Assert.AreEqual(1, kosdaq.Count);
  86. Assert.AreEqual(StockMarket.KOSDAQ, kosdaq[0].Market);
  87. Assert.AreEqual("우량기업부", kosdaq[0].SectorName, "KOSDAQ SECT_TP_NM(소속부) → SectorName");
  88. }
  89. [TestMethod]
  90. public void ParseMasterInfo_InvalidIsin_And_EmptyBlock()
  91. {
  92. // ISU_CD 가 12자리가 아니면 ISIN=null (방어)
  93. const string json = """
  94. {"OutBlock_1":[
  95. {"ISU_CD":"BADISIN","ISU_SRT_CD":"005930","ISU_NM":"삼성전자보통주","ISU_ABBRV":"삼성전자","ISU_ENG_NM":"","LIST_DD":"19750611","MKT_TP_NM":"KOSPI","SECT_TP_NM":"","LIST_SHRS":"1"}
  96. ]}
  97. """;
  98. var rows = KrxStockParser.ParseMasterInfo(json, StockMarket.KOSPI);
  99. Assert.AreEqual(1, rows.Count);
  100. Assert.IsNull(rows[0].Isin, "12자리 아닌 표준코드는 ISIN=null");
  101. Assert.IsNull(rows[0].EnglishName, "빈 ISU_ENG_NM 은 null");
  102. // 빈 스냅샷 / 노드 없음 안전 처리
  103. Assert.AreEqual(0, KrxStockParser.ParseMasterInfo("""{"OutBlock_1":[]}""", StockMarket.KOSPI).Count);
  104. Assert.AreEqual(0, KrxStockParser.ParseMasterInfo("""{"respCode":"401"}""", StockMarket.KOSDAQ).Count);
  105. }
  106. }