SeibroDerivImport.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. using Application.Abstractions.Data;
  2. using Domain.Entities.Stocks;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Infrastructure.StockData;
  5. /// <summary>
  6. /// SeibroDerivSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다
  7. /// (SeibroBondImport 패턴 미러링). 일곱 엔티티(발행·마스터·기초자산·기초자산행사·상환조건·상환종목·미상환규모) upsert 담당.
  8. /// getDerivCombiIsinInfo/getAssetXrcInfo/getRedCondiInfo 응답에는 ISIN 이 없어(요청 파라미터로만 받음) 배치가 요청 ISIN 을 스탬핑한다.
  9. /// getDerivCombiIssuInfo/getRedIsinInfo 는 SECN_KACD·(ISSU_DT|RED_DT) 요청값을, getBassetUnredScale 는 STD_DT·STND_BASSET_CD·SECN_TPCD 를 스탬핑한다.
  10. /// </summary>
  11. public static class SeibroDerivImport
  12. {
  13. /// <summary>getDerivCombiIssuInfo 결과를 DerivIssuance 로 upsert — UQ (Isin). SECN_KACD·ISSU_DT 요청값 스탬핑. rows 는 발행일×종류 스윕 per 콜 결과(다수 종목).</summary>
  14. public static async Task<(int Inserted, int Updated)> UpsertDerivIssuancesAsync(IAppDbContext db, string secnKacd, DateOnly issuDt, IReadOnlyList<SeibroDerivCombiIssuParser.Row> rows, CancellationToken ct)
  15. {
  16. if (rows.Count == 0)
  17. {
  18. return (0, 0);
  19. }
  20. var isins = rows.Select(c => c.Isin).Distinct().ToList();
  21. var existing = await db.DerivIssuance.Where(c => isins.Contains(c.Isin)).ToListAsync(ct);
  22. var byKey = existing.ToDictionary(c => c.Isin);
  23. var seen = new HashSet<string>();
  24. var inserted = 0;
  25. var updated = 0;
  26. foreach (var row in rows)
  27. {
  28. if (!seen.Add(row.Isin))
  29. {
  30. continue;
  31. }
  32. if (byKey.TryGetValue(row.Isin, out var entity))
  33. {
  34. var was = entity.UpdatedAt;
  35. entity.Update(row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt);
  36. if (entity.UpdatedAt != was)
  37. {
  38. updated++;
  39. }
  40. }
  41. else
  42. {
  43. var created = DerivIssuance.Create(row.Isin, row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt);
  44. await db.DerivIssuance.AddAsync(created, ct);
  45. byKey[row.Isin] = created;
  46. inserted++;
  47. }
  48. }
  49. await db.SaveChangesAsync(ct);
  50. return (inserted, updated);
  51. }
  52. /// <summary>getDerivCombiIsinInfo 결과를 DerivMaster 로 upsert — PK Isin(요청값 스탬핑). rolling per-ISIN 1콜 결과(첫 행만). 빈결과면 no-op.</summary>
  53. public static async Task<bool> UpsertDerivMasterAsync(IAppDbContext db, string isin, IReadOnlyList<DerivMasterFields> rows, CancellationToken ct)
  54. {
  55. if (rows.Count == 0)
  56. {
  57. return false;
  58. }
  59. var fields = rows[0];
  60. var entity = await db.DerivMaster.FirstOrDefaultAsync(c => c.Isin == isin, ct);
  61. if (entity is null)
  62. {
  63. await db.DerivMaster.AddAsync(DerivMaster.Create(isin, fields), ct);
  64. }
  65. else
  66. {
  67. entity.Update(fields);
  68. }
  69. await db.SaveChangesAsync(ct);
  70. return true;
  71. }
  72. /// <summary>getAssetInfo 결과를 DerivAsset 으로 upsert — UQ (Isin, AssetSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 기초자산).</summary>
  73. public static async Task<(int Inserted, int Updated)> UpsertDerivAssetsAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroAssetParser.Row> rows, CancellationToken ct)
  74. {
  75. if (rows.Count == 0)
  76. {
  77. return (0, 0);
  78. }
  79. var existing = await db.DerivAsset.Where(c => c.Isin == isin).ToListAsync(ct);
  80. var byKey = existing.ToDictionary(c => c.AssetSeq);
  81. var seen = new HashSet<int>();
  82. var inserted = 0;
  83. var updated = 0;
  84. foreach (var row in rows)
  85. {
  86. if (!seen.Add(row.AssetSeq))
  87. {
  88. continue;
  89. }
  90. if (byKey.TryGetValue(row.AssetSeq, out var entity))
  91. {
  92. var was = entity.UpdatedAt;
  93. entity.Update(row.Fields);
  94. if (entity.UpdatedAt != was)
  95. {
  96. updated++;
  97. }
  98. }
  99. else
  100. {
  101. var created = DerivAsset.Create(isin, row.AssetSeq, row.Fields);
  102. await db.DerivAsset.AddAsync(created, ct);
  103. byKey[row.AssetSeq] = created;
  104. inserted++;
  105. }
  106. }
  107. await db.SaveChangesAsync(ct);
  108. return (inserted, updated);
  109. }
  110. /// <summary>getAssetXrcInfo 결과를 DerivAssetExercise 로 upsert — UQ (Isin, AssetSeq, XrcStdSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 행).</summary>
  111. public static async Task<(int Inserted, int Updated)> UpsertDerivAssetExercisesAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroAssetXrcParser.Row> rows, CancellationToken ct)
  112. {
  113. if (rows.Count == 0)
  114. {
  115. return (0, 0);
  116. }
  117. var existing = await db.DerivAssetExercise.Where(c => c.Isin == isin).ToListAsync(ct);
  118. var byKey = existing.ToDictionary(c => (c.AssetSeq, c.XrcStdSeq));
  119. var seen = new HashSet<(int, int)>();
  120. var inserted = 0;
  121. var updated = 0;
  122. foreach (var row in rows)
  123. {
  124. var key = (row.AssetSeq, row.XrcStdSeq);
  125. if (!seen.Add(key))
  126. {
  127. continue;
  128. }
  129. if (byKey.TryGetValue(key, out var entity))
  130. {
  131. var was = entity.UpdatedAt;
  132. entity.Update(row.XrcStdRatio, row.XrcPrice);
  133. if (entity.UpdatedAt != was)
  134. {
  135. updated++;
  136. }
  137. }
  138. else
  139. {
  140. var created = DerivAssetExercise.Create(isin, row.AssetSeq, row.XrcStdSeq, row.XrcStdRatio, row.XrcPrice);
  141. await db.DerivAssetExercise.AddAsync(created, ct);
  142. byKey[key] = created;
  143. inserted++;
  144. }
  145. }
  146. await db.SaveChangesAsync(ct);
  147. return (inserted, updated);
  148. }
  149. /// <summary>getRedCondiInfo 결과를 DerivRedemptionCondition 으로 upsert — UQ (Isin, RedCondiTpcd, ValatNtimesSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 조건).</summary>
  150. public static async Task<(int Inserted, int Updated)> UpsertDerivRedemptionConditionsAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroRedCondiParser.Row> rows, CancellationToken ct)
  151. {
  152. if (rows.Count == 0)
  153. {
  154. return (0, 0);
  155. }
  156. var existing = await db.DerivRedemptionCondition.Where(c => c.Isin == isin).ToListAsync(ct);
  157. var byKey = existing.ToDictionary(c => (c.RedCondiTpcd, c.ValatNtimesSeq));
  158. var seen = new HashSet<(string, int)>();
  159. var inserted = 0;
  160. var updated = 0;
  161. foreach (var row in rows)
  162. {
  163. var key = (row.RedCondiTpcd, row.ValatNtimesSeq);
  164. if (!seen.Add(key))
  165. {
  166. continue;
  167. }
  168. if (byKey.TryGetValue(key, out var entity))
  169. {
  170. var was = entity.UpdatedAt;
  171. entity.Update(row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent);
  172. if (entity.UpdatedAt != was)
  173. {
  174. updated++;
  175. }
  176. }
  177. else
  178. {
  179. var created = DerivRedemptionCondition.Create(isin, row.RedCondiTpcd, row.ValatNtimesSeq, row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent);
  180. await db.DerivRedemptionCondition.AddAsync(created, ct);
  181. byKey[key] = created;
  182. inserted++;
  183. }
  184. }
  185. await db.SaveChangesAsync(ct);
  186. return (inserted, updated);
  187. }
  188. /// <summary>
  189. /// getRedIsinInfo 결과를 DerivRedemption 으로 window(상환일 begin~end)×종류 delete+insert 반영.
  190. /// 날짜 스윕(RED_DT)이라 창 재구성으로 삭제분 반영. RED_DT·SECN_KACD 요청값 스탬핑. 같은 창·종류 내 (Isin, RedDt, DerisecExerTpcd) 중복 방어.
  191. /// </summary>
  192. public static async Task<(int Inserted, int Deleted)> ReplaceRedemptionWindowAsync(IAppDbContext db, string secnKacd, DateOnly begin, DateOnly end, IReadOnlyList<SeibroRedIsinParser.Row> rows, CancellationToken ct)
  193. {
  194. var deleted = await db.DerivRedemption.Where(c => c.SecnKacd == secnKacd && c.RedDt >= begin && c.RedDt <= end).ExecuteDeleteAsync(ct);
  195. var seen = new HashSet<(string, DateOnly, string)>();
  196. var inserted = 0;
  197. foreach (var row in rows)
  198. {
  199. // 날짜 스윕은 begin=end 하루라 RedDt 는 begin 으로 스탬핑
  200. var key = (row.Isin, begin, row.DerisecExerTpcd);
  201. if (!seen.Add(key))
  202. {
  203. continue;
  204. }
  205. var created = DerivRedemption.Create(row.Isin, row.DerisecExerTpcd, row.RedQty, begin, secnKacd);
  206. await db.DerivRedemption.AddAsync(created, ct);
  207. inserted++;
  208. }
  209. await db.SaveChangesAsync(ct);
  210. return (inserted, deleted);
  211. }
  212. /// <summary>getBassetUnredScale 결과를 ElsUnredeemedScale 로 upsert — UQ (StdDt, StndBassetCd, SecnTpcd). 요청값 3종 스탬핑. 종목당 1행(첫 행만). 빈결과면 no-op.</summary>
  213. public static async Task<bool> UpsertUnredeemedScaleAsync(IAppDbContext db, DateOnly stdDt, string stndBassetCd, string secnTpcd, IReadOnlyList<SeibroBassetUnredScaleParser.Row> rows, CancellationToken ct)
  214. {
  215. if (rows.Count == 0)
  216. {
  217. return false;
  218. }
  219. var row = rows[0];
  220. var entity = await db.ElsUnredeemedScale.FirstOrDefaultAsync(c => c.StdDt == stdDt && c.StndBassetCd == stndBassetCd && c.SecnTpcd == secnTpcd, ct);
  221. if (entity is null)
  222. {
  223. await db.ElsUnredeemedScale.AddAsync(ElsUnredeemedScale.Create(stdDt, stndBassetCd, secnTpcd, row.BassetContent, row.UnredSecncnt, row.UnredRema), ct);
  224. }
  225. else
  226. {
  227. entity.Update(row.BassetContent, row.UnredSecncnt, row.UnredRema);
  228. }
  229. await db.SaveChangesAsync(ct);
  230. return true;
  231. }
  232. }