DerivativeSessionUpsertTests.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. using Domain.Entities.Stocks;
  2. using Domain.Entities.Stocks.ValueObject;
  3. using Microsoft.EntityFrameworkCore;
  4. using FuturesHandler = Application.Features.Api.Stocks.GetFutures.Handler;
  5. using FuturesQuery = Application.Features.Api.Stocks.GetFutures.Query;
  6. namespace Application.Tests;
  7. /// <summary>
  8. /// 파생상품 정규·야간 세션 분리 저장 검증 — UQ 가 (Kind, IsuCode, TradeDate, Session) 으로 확장되어
  9. /// 동일 종목코드의 정규·야간 행이 각각 저장되고(야간이 정규를 덮어쓰지 않음), 같은 세션 중복은 UQ 로 차단된다.
  10. /// </summary>
  11. [TestClass]
  12. public sealed class DerivativeSessionUpsertTests
  13. {
  14. [TestMethod]
  15. public async Task Futures_SameCode_RegularAndNight_BothPersisted()
  16. {
  17. // 기대 행위: 동일 (FuturesKind, IsuCode, TradeDate) 의 정규·야간 행이 각각 저장된다 (기존엔 야간이 정규를 덮어씀).
  18. using var db = TestDb.Create();
  19. var day = new DateOnly(2026, 3, 4);
  20. var regular = FuturesDailyTrade.Create(FuturesKind.General, "ZQF69000", "10년국채 F 202609 (주간)", day, MarketSession.Regular, 106.29m, 106.20m, 106.35m, 106.15m, 0.07m, 106.30m, 106.29m, 82224L, 8741234560000L, 215549L, "10년국채 선물", "정규");
  21. var night = FuturesDailyTrade.Create(FuturesKind.General, "ZQF69000", "10년국채 F 202609 (야간)", day, MarketSession.Night, 106.22m, 106.28m, 106.28m, 106.20m, -0.07m, null, 106.22m, 30L, 3186600000L, null, "10년국채 선물", "야간");
  22. await db.FuturesDailyTrade.AddAsync(regular);
  23. await db.FuturesDailyTrade.AddAsync(night);
  24. await db.SaveChangesAsync(default);
  25. var saved = await db.FuturesDailyTrade.AsNoTracking().Where(c => c.IsuCode == "ZQF69000" && c.TradeDate == day).OrderBy(c => c.Session).ToListAsync();
  26. Assert.AreEqual(2, saved.Count, "정규·야간이 별도 행으로 저장된다");
  27. Assert.AreEqual(MarketSession.Regular, saved[0].Session);
  28. Assert.AreEqual(106.29m, saved[0].Close, "정규 종가가 야간 값으로 오염되지 않는다");
  29. Assert.AreEqual(82224L, saved[0].Volume, "정규 거래량이 야간 값으로 오염되지 않는다");
  30. Assert.AreEqual(MarketSession.Night, saved[1].Session);
  31. Assert.AreEqual(106.22m, saved[1].Close);
  32. Assert.AreEqual(30L, saved[1].Volume);
  33. }
  34. [TestMethod]
  35. public async Task Futures_SameSessionDuplicate_RejectedByUniqueIndex()
  36. {
  37. // 기대 행위: 같은 (Kind, IsuCode, TradeDate, Session) 중복 insert 는 UQ 위반으로 차단된다.
  38. var day = new DateOnly(2026, 3, 5);
  39. using (var db = TestDb.Create())
  40. {
  41. await db.FuturesDailyTrade.AddAsync(FuturesDailyTrade.Create(FuturesKind.General, "ZQF69001", "테스트 F 202609", day, MarketSession.Regular, 1m, 1m, 1m, 1m, 0m, null, null, 0L, 0L, null));
  42. await db.SaveChangesAsync(default);
  43. }
  44. using (var db = TestDb.Create())
  45. {
  46. await db.FuturesDailyTrade.AddAsync(FuturesDailyTrade.Create(FuturesKind.General, "ZQF69001", "테스트 F 202609", day, MarketSession.Regular, 2m, 2m, 2m, 2m, 0m, null, null, 0L, 0L, null));
  47. await Assert.ThrowsExactlyAsync<DbUpdateException>(async () => await db.SaveChangesAsync(default), "동일 세션 중복은 UQ 로 차단");
  48. }
  49. }
  50. [TestMethod]
  51. public async Task Options_SameCode_RegularAndNight_BothPersisted()
  52. {
  53. // 기대 행위: 옵션도 동일 (OptionsKind, IsuCode, TradeDate) 의 정규·야간 행이 각각 저장된다 (옵션 응답엔 MKT_NM 없이 종목명 표기).
  54. using var db = TestDb.Create();
  55. var day = new DateOnly(2026, 3, 4);
  56. var regular = OptionsDailyTrade.Create(OptionsKind.General, "ZQO51232", "미니코스피 C 202607 232.5 (정규)", day, MarketSession.Regular, OptionRightType.Call, 232.5m, 1.50m, 1.40m, 1.55m, 1.38m, 0.10m, 12.00m, 1.50m, 1000L, 150000000L, 500L, "미니코스피200 옵션");
  57. var night = OptionsDailyTrade.Create(OptionsKind.General, "ZQO51232", "미니코스피 C 202607 232.5 (야간)", day, MarketSession.Night, OptionRightType.Call, 232.5m, 1.45m, 1.50m, 1.50m, 1.44m, -0.05m, 12.10m, 1.45m, 10L, 1500000L, 500L, "미니코스피200 옵션");
  58. await db.OptionsDailyTrade.AddAsync(regular);
  59. await db.OptionsDailyTrade.AddAsync(night);
  60. await db.SaveChangesAsync(default);
  61. var saved = await db.OptionsDailyTrade.AsNoTracking().Where(c => c.IsuCode == "ZQO51232" && c.TradeDate == day).OrderBy(c => c.Session).ToListAsync();
  62. Assert.AreEqual(2, saved.Count, "정규·야간이 별도 행으로 저장된다");
  63. Assert.AreEqual(1.50m, saved[0].Close, "정규 종가 보존");
  64. Assert.AreEqual(1.45m, saved[1].Close, "야간 종가 보존");
  65. }
  66. [TestMethod]
  67. public async Task GetFutures_DefaultRegularOnly_NightBySessionParameter()
  68. {
  69. // 기대 행위: 조회 API 는 기본 정규 세션만 반환하고, session 파라미터로 야간을 조회할 수 있다.
  70. using var db = TestDb.Create();
  71. var day = new DateOnly(2026, 3, 6);
  72. await db.FuturesDailyTrade.AddAsync(FuturesDailyTrade.Create(FuturesKind.General, "ZQF69002", "테스트2 F 202609 (주간)", day, MarketSession.Regular, 100m, 100m, 100m, 100m, 0m, null, null, 500L, 50000L, null));
  73. await db.FuturesDailyTrade.AddAsync(FuturesDailyTrade.Create(FuturesKind.General, "ZQF69002", "테스트2 F 202609 (야간)", day, MarketSession.Night, 99m, 99m, 99m, 99m, -1m, null, null, 5L, 495L, null));
  74. await db.SaveChangesAsync(default);
  75. var handler = new FuturesHandler(db);
  76. var regularResult = await handler.Handle(new FuturesQuery(FuturesKind.General, day, 1, 100), default);
  77. Assert.IsTrue(regularResult.List.All(c => c.Session == MarketSession.Regular), "기본 조회는 정규 세션만 반환");
  78. Assert.IsTrue(regularResult.List.Any(c => c.IsuCode == "ZQF69002" && c.Close == 100m), "정규 행 반환");
  79. var nightResult = await handler.Handle(new FuturesQuery(FuturesKind.General, day, 1, 100, MarketSession.Night), default);
  80. Assert.IsTrue(nightResult.List.All(c => c.Session == MarketSession.Night), "session=2 조회는 야간 세션만 반환");
  81. Assert.IsTrue(nightResult.List.Any(c => c.IsuCode == "ZQF69002" && c.Close == 99m), "야간 행 반환");
  82. }
  83. }