SeibroDerivSyncTests.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. using Application.Helpers;
  2. using Domain.Entities.Stocks;
  3. using Infrastructure.StockData;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace Application.Tests;
  6. /// <summary>
  7. /// SeibroDerivSyncService 의 DB 반영 로직(SeibroDerivImport) + rolling 대상 선정 통합 검증 —
  8. /// HTTP 는 파서 입력을 fixture/합성 XML 로 대체(SeibroBondSyncTests 방식 미러링). 실제 SQL Server(LocalDB).
  9. /// 발견체인(발행→마스터 rolling) / 기초자산·행사·상환조건 upsert / 상환종목 window(종류별) / 미상환규모 13×2 스탬핑 / Deriv 예산 상한.
  10. /// </summary>
  11. [TestClass]
  12. public sealed class SeibroDerivSyncTests
  13. {
  14. [TestMethod]
  15. public async Task UpsertDerivIssuances_FromSampleXml_StampsSecnKacdAndIssuDt_ThenIdempotent()
  16. {
  17. // 기대 행위: getDerivCombiIssuInfo(다수행) — 응답에 없는 SECN_KACD·ISSU_DT 를 요청값 스탬핑 upsert (UQ Isin).
  18. var rows = SeibroDerivCombiIssuParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIssuInfo.xml")));
  19. var issuDt = new DateOnly(2018, 7, 2);
  20. using (var db = TestDb.Create())
  21. {
  22. var (inserted, _) = await SeibroDerivImport.UpsertDerivIssuancesAsync(db, "4101", issuDt, rows, CancellationToken.None);
  23. Assert.IsTrue(inserted > 1, "다수 발행종목 insert");
  24. }
  25. using (var db = TestDb.Create())
  26. {
  27. var (inserted, updated) = await SeibroDerivImport.UpsertDerivIssuancesAsync(db, "4101", issuDt, rows, CancellationToken.None);
  28. Assert.AreEqual(0, inserted, "재실행 idempotent");
  29. Assert.AreEqual(0, updated, "무변경 no-op");
  30. var saved = await db.DerivIssuance.AsNoTracking().Where(c => c.Isin == "KR6533308872").SingleAsync();
  31. Assert.AreEqual("4101", saved.SecnKacd, "종류 스탬핑");
  32. Assert.AreEqual(issuDt, saved.IssuDt, "발행일 스탬핑");
  33. Assert.AreEqual(1636, saved.IssucoCustno);
  34. }
  35. }
  36. [TestMethod]
  37. public async Task DerivMasterTarget_ComesFromDerivIssuance_AndUpsertStampsIsin()
  38. {
  39. // 발견체인: DerivMaster rolling 대상 = DerivIssuance.Isin. getDerivCombiIsinInfo 응답엔 ISIN 없어 요청값 스탬핑, PK Isin upsert.
  40. var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}";
  41. using (var db = TestDb.Create())
  42. {
  43. await db.DerivIssuance.AddAsync(DerivIssuance.Create(isin, "테스트ELS", 1636, "4101", new DateOnly(2018, 7, 2)), CancellationToken.None);
  44. await db.SaveChangesAsync(CancellationToken.None);
  45. }
  46. using (var db = TestDb.Create())
  47. {
  48. // rolling 대상 = DerivIssuance ∖ DerivMaster (미수집 우선)
  49. var issued = await db.DerivIssuance.AsNoTracking().Select(c => c.Isin).ToListAsync();
  50. CollectionAssert.Contains(issued, isin);
  51. var rows = SeibroDerivCombiIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIsinInfo.xml")));
  52. Assert.IsTrue(await SeibroDerivImport.UpsertDerivMasterAsync(db, isin, rows, CancellationToken.None));
  53. }
  54. using (var db = TestDb.Create())
  55. {
  56. Assert.IsTrue(await SeibroDerivImport.UpsertDerivMasterAsync(db, isin, SeibroDerivCombiIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIsinInfo.xml"))), CancellationToken.None), "재수집 idempotent upsert");
  57. var saved = await db.DerivMaster.AsNoTracking().Where(c => c.Isin == isin).SingleAsync();
  58. Assert.AreEqual("ELS", saved.SecnKanm);
  59. Assert.AreEqual(new DateOnly(2021, 7, 2), saved.XpirDt, "만기");
  60. Assert.AreEqual("3", saved.PrcpPrsvTpcd, "원금비보장");
  61. Assert.AreEqual(3, saved.BassetCnt);
  62. Assert.IsTrue(saved.MidredPossYn);
  63. }
  64. }
  65. [TestMethod]
  66. public async Task UpsertDerivAssets_FromSampleXml_StampsIsin_PreservesRawBassetCode()
  67. {
  68. // 기대 행위: getAssetInfo(3행) — ISIN 요청값 스탬핑, UQ(Isin,AssetSeq) upsert, StndBassetCd 반환 원문 보존.
  69. var rows = SeibroAssetParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getAssetInfo.xml")));
  70. var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}";
  71. using (var db = TestDb.Create())
  72. {
  73. var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetsAsync(db, isin, rows, CancellationToken.None);
  74. Assert.AreEqual(3, inserted, "기초자산 3종");
  75. }
  76. using (var db = TestDb.Create())
  77. {
  78. var (inserted, updated) = await SeibroDerivImport.UpsertDerivAssetsAsync(db, isin, rows, CancellationToken.None);
  79. Assert.AreEqual(0, inserted, "재실행 idempotent");
  80. Assert.AreEqual(0, updated);
  81. var hscei = await db.DerivAsset.AsNoTracking().Where(c => c.Isin == isin && c.AssetSeq == 1).SingleAsync();
  82. Assert.AreEqual("KSD310000306", hscei.StndBassetCd, "반환 코드(KSD31*) 원문 저장");
  83. Assert.AreEqual(11073m, hscei.XrcStdprc);
  84. }
  85. }
  86. [TestMethod]
  87. public async Task UpsertDerivAssetExercises_FromSampleXml_StampsIsin()
  88. {
  89. // 기대 행위: getAssetXrcInfo(8행) — ISIN 요청값 스탬핑, UQ(Isin,AssetSeq,XrcStdSeq) upsert.
  90. var rows = SeibroAssetXrcParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getAssetXrcInfo.xml")));
  91. var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}";
  92. using (var db = TestDb.Create())
  93. {
  94. var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetExercisesAsync(db, isin, rows, CancellationToken.None);
  95. Assert.AreEqual(8, inserted);
  96. }
  97. using (var db = TestDb.Create())
  98. {
  99. var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetExercisesAsync(db, isin, rows, CancellationToken.None);
  100. Assert.AreEqual(0, inserted, "재실행 idempotent");
  101. var last = await db.DerivAssetExercise.AsNoTracking().Where(c => c.Isin == isin && c.XrcStdSeq == 8).SingleAsync();
  102. Assert.AreEqual(65m, last.XrcStdRatio);
  103. Assert.AreEqual(181.2395m, last.XrcPrice);
  104. }
  105. }
  106. [TestMethod]
  107. public async Task UpsertRedemptionConditions_FromSampleXml_StampsIsin_1000CharText()
  108. {
  109. // 기대 행위: getRedCondiInfo(10행) — ISIN 요청값 스탬핑, UQ(Isin,RedCondiTpcd,ValatNtimesSeq) upsert, 1000자 텍스트 보존.
  110. var rows = SeibroRedCondiParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getRedCondiInfo.xml")));
  111. var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}";
  112. using (var db = TestDb.Create())
  113. {
  114. var (inserted, _) = await SeibroDerivImport.UpsertDerivRedemptionConditionsAsync(db, isin, rows, CancellationToken.None);
  115. Assert.AreEqual(10, inserted, "중간 8 + 만기 2");
  116. }
  117. using (var db = TestDb.Create())
  118. {
  119. var (inserted, _) = await SeibroDerivImport.UpsertDerivRedemptionConditionsAsync(db, isin, rows, CancellationToken.None);
  120. Assert.AreEqual(0, inserted, "재실행 idempotent");
  121. var expiryLoss = await db.DerivRedemptionCondition.AsNoTracking().Where(c => c.Isin == isin && c.RedCondiTpcd == "2" && c.ValatNtimesSeq == 2).SingleAsync();
  122. StringAssert.Contains(expiryLoss.RedCondiContent, "65% 미만");
  123. Assert.AreEqual("액면금액 * (만기평가가격/최초기준가격)", expiryLoss.RedFormulaContent);
  124. }
  125. }
  126. [TestMethod]
  127. public async Task ReplaceRedemptionWindow_FromSampleXml_StampsRedDtAndSecnKacd_ThenReconstructs()
  128. {
  129. // 기대 행위: getRedIsinInfo — 상환일×종류 window delete+insert (UQ Isin+RedDt+DerisecExerTpcd, RED_DT·SECN_KACD 스탬핑). 재실행 창 재구성.
  130. var rows = SeibroRedIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getRedIsinInfo.xml")));
  131. var day = new DateOnly(2018, 8, 6);
  132. using (var db = TestDb.Create())
  133. {
  134. var (inserted, deleted) = await SeibroDerivImport.ReplaceRedemptionWindowAsync(db, "4101", day, day, rows, CancellationToken.None);
  135. Assert.AreEqual(12, inserted);
  136. Assert.AreEqual(0, deleted);
  137. }
  138. using (var db = TestDb.Create())
  139. {
  140. var (inserted, deleted) = await SeibroDerivImport.ReplaceRedemptionWindowAsync(db, "4101", day, day, rows, CancellationToken.None);
  141. Assert.AreEqual(12, inserted, "창 재구성");
  142. Assert.AreEqual(12, deleted, "기존 창 삭제");
  143. var saved = await db.DerivRedemption.AsNoTracking().Where(c => c.Isin == "KR6552327845" && c.RedDt == day).SingleAsync();
  144. Assert.AreEqual("4101", saved.SecnKacd, "종류 스탬핑");
  145. Assert.AreEqual("3", saved.DerisecExerTpcd, "조기");
  146. Assert.AreEqual(132000000m, saved.RedQty);
  147. }
  148. }
  149. [TestMethod]
  150. public async Task UpsertUnredeemedScale_13x2_StampsAllKeys_EmptyRemaTolerated()
  151. {
  152. // 기대 행위: getBassetUnredScale — STD_DT·STND_BASSET_CD·SECN_TPCD 3종 스탬핑, UQ upsert. 실샘플 UNRED_REMA 빈값 → null 허용.
  153. var rows = SeibroBassetUnredScaleParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getBassetUnredScale.xml")));
  154. // 공유 DB 라 고정 기준일이 타 테스트와 충돌하지 않도록 이 테스트 전용 기초자산코드 프리픽스를 쓴다.
  155. var stdDt = new DateOnly(2018, 7, 31);
  156. var cdA = $"TSTA{Guid.NewGuid().ToString("N")[..8]}";
  157. var cdB = $"TSTB{Guid.NewGuid().ToString("N")[..8]}";
  158. using (var db = TestDb.Create())
  159. {
  160. // 같은 기준일에 서로 다른 기초자산×종목구분 조합이 각각 별 row 로 들어감 (13×2 스윕의 축소판)
  161. Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "41", rows, CancellationToken.None));
  162. Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "43", rows, CancellationToken.None), "같은 기초자산 다른 SECN_TPCD 는 별 row");
  163. Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdB, "41", rows, CancellationToken.None), "다른 기초자산");
  164. }
  165. using (var db = TestDb.Create())
  166. {
  167. var mine = await db.ElsUnredeemedScale.AsNoTracking().Where(c => c.StdDt == stdDt && (c.StndBassetCd == cdA || c.StndBassetCd == cdB)).ToListAsync();
  168. Assert.AreEqual(3, mine.Count, "13×2 조합 중 3건 스탬핑 (본 테스트 기초자산만)");
  169. var kospi41 = mine.Single(c => c.StndBassetCd == cdA && c.SecnTpcd == "41");
  170. Assert.AreEqual("EURO STOXX 50", kospi41.BassetContent, "응답 기초자산내용 그대로");
  171. Assert.AreEqual(2, kospi41.UnredSecncnt);
  172. Assert.IsNull(kospi41.UnredRema, "빈 잔액 → null");
  173. // 재실행 idempotent
  174. Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "41", rows, CancellationToken.None));
  175. var again = await db.ElsUnredeemedScale.AsNoTracking().Where(c => c.StdDt == stdDt && (c.StndBassetCd == cdA || c.StndBassetCd == cdB)).CountAsync();
  176. Assert.AreEqual(3, again, "재실행 무증가 (upsert)");
  177. }
  178. }
  179. [TestMethod]
  180. public void Quota_DerivBudget_CapsConsumption()
  181. {
  182. // 기대 행위: 파생 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향.
  183. var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7));
  184. Assert.IsTrue(quota.TryConsume(SeibroCategory.Deriv, 8000, 8000), "예산 전량 소비");
  185. Assert.IsFalse(quota.TryConsume(SeibroCategory.Deriv, 1, 8000), "파생 예산 소진");
  186. Assert.AreEqual(0, quota.Remaining(SeibroCategory.Deriv, 8000));
  187. Assert.AreEqual(8000, quota.Remaining(SeibroCategory.Bond, 8000), "채권 예산 무영향");
  188. }
  189. }