| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- using Application.Abstractions.Data;
- using Domain.Entities.Stocks;
- using Microsoft.EntityFrameworkCore;
- namespace Infrastructure.StockData;
- /// <summary>
- /// SeibroDerivSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다
- /// (SeibroBondImport 패턴 미러링). 일곱 엔티티(발행·마스터·기초자산·기초자산행사·상환조건·상환종목·미상환규모) upsert 담당.
- /// getDerivCombiIsinInfo/getAssetXrcInfo/getRedCondiInfo 응답에는 ISIN 이 없어(요청 파라미터로만 받음) 배치가 요청 ISIN 을 스탬핑한다.
- /// getDerivCombiIssuInfo/getRedIsinInfo 는 SECN_KACD·(ISSU_DT|RED_DT) 요청값을, getBassetUnredScale 는 STD_DT·STND_BASSET_CD·SECN_TPCD 를 스탬핑한다.
- /// </summary>
- public static class SeibroDerivImport
- {
- /// <summary>getDerivCombiIssuInfo 결과를 DerivIssuance 로 upsert — UQ (Isin). SECN_KACD·ISSU_DT 요청값 스탬핑. rows 는 발행일×종류 스윕 per 콜 결과(다수 종목).</summary>
- public static async Task<(int Inserted, int Updated)> UpsertDerivIssuancesAsync(IAppDbContext db, string secnKacd, DateOnly issuDt, IReadOnlyList<SeibroDerivCombiIssuParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var isins = rows.Select(c => c.Isin).Distinct().ToList();
- var existing = await db.DerivIssuance.Where(c => isins.Contains(c.Isin)).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => c.Isin);
- var seen = new HashSet<string>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- if (!seen.Add(row.Isin))
- {
- continue;
- }
- if (byKey.TryGetValue(row.Isin, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = DerivIssuance.Create(row.Isin, row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt);
- await db.DerivIssuance.AddAsync(created, ct);
- byKey[row.Isin] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>getDerivCombiIsinInfo 결과를 DerivMaster 로 upsert — PK Isin(요청값 스탬핑). rolling per-ISIN 1콜 결과(첫 행만). 빈결과면 no-op.</summary>
- public static async Task<bool> UpsertDerivMasterAsync(IAppDbContext db, string isin, IReadOnlyList<DerivMasterFields> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return false;
- }
- var fields = rows[0];
- var entity = await db.DerivMaster.FirstOrDefaultAsync(c => c.Isin == isin, ct);
- if (entity is null)
- {
- await db.DerivMaster.AddAsync(DerivMaster.Create(isin, fields), ct);
- }
- else
- {
- entity.Update(fields);
- }
- await db.SaveChangesAsync(ct);
- return true;
- }
- /// <summary>getAssetInfo 결과를 DerivAsset 으로 upsert — UQ (Isin, AssetSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 기초자산).</summary>
- public static async Task<(int Inserted, int Updated)> UpsertDerivAssetsAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroAssetParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var existing = await db.DerivAsset.Where(c => c.Isin == isin).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => c.AssetSeq);
- var seen = new HashSet<int>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- if (!seen.Add(row.AssetSeq))
- {
- continue;
- }
- if (byKey.TryGetValue(row.AssetSeq, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.Fields);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = DerivAsset.Create(isin, row.AssetSeq, row.Fields);
- await db.DerivAsset.AddAsync(created, ct);
- byKey[row.AssetSeq] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>getAssetXrcInfo 결과를 DerivAssetExercise 로 upsert — UQ (Isin, AssetSeq, XrcStdSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 행).</summary>
- public static async Task<(int Inserted, int Updated)> UpsertDerivAssetExercisesAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroAssetXrcParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var existing = await db.DerivAssetExercise.Where(c => c.Isin == isin).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.AssetSeq, c.XrcStdSeq));
- var seen = new HashSet<(int, int)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.AssetSeq, row.XrcStdSeq);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.XrcStdRatio, row.XrcPrice);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = DerivAssetExercise.Create(isin, row.AssetSeq, row.XrcStdSeq, row.XrcStdRatio, row.XrcPrice);
- await db.DerivAssetExercise.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>getRedCondiInfo 결과를 DerivRedemptionCondition 으로 upsert — UQ (Isin, RedCondiTpcd, ValatNtimesSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 조건).</summary>
- public static async Task<(int Inserted, int Updated)> UpsertDerivRedemptionConditionsAsync(IAppDbContext db, string isin, IReadOnlyList<SeibroRedCondiParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var existing = await db.DerivRedemptionCondition.Where(c => c.Isin == isin).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.RedCondiTpcd, c.ValatNtimesSeq));
- var seen = new HashSet<(string, int)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.RedCondiTpcd, row.ValatNtimesSeq);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = DerivRedemptionCondition.Create(isin, row.RedCondiTpcd, row.ValatNtimesSeq, row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent);
- await db.DerivRedemptionCondition.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getRedIsinInfo 결과를 DerivRedemption 으로 window(상환일 begin~end)×종류 delete+insert 반영.
- /// 날짜 스윕(RED_DT)이라 창 재구성으로 삭제분 반영. RED_DT·SECN_KACD 요청값 스탬핑. 같은 창·종류 내 (Isin, RedDt, DerisecExerTpcd) 중복 방어.
- /// </summary>
- public static async Task<(int Inserted, int Deleted)> ReplaceRedemptionWindowAsync(IAppDbContext db, string secnKacd, DateOnly begin, DateOnly end, IReadOnlyList<SeibroRedIsinParser.Row> rows, CancellationToken ct)
- {
- var deleted = await db.DerivRedemption.Where(c => c.SecnKacd == secnKacd && c.RedDt >= begin && c.RedDt <= end).ExecuteDeleteAsync(ct);
- var seen = new HashSet<(string, DateOnly, string)>();
- var inserted = 0;
- foreach (var row in rows)
- {
- // 날짜 스윕은 begin=end 하루라 RedDt 는 begin 으로 스탬핑
- var key = (row.Isin, begin, row.DerisecExerTpcd);
- if (!seen.Add(key))
- {
- continue;
- }
- var created = DerivRedemption.Create(row.Isin, row.DerisecExerTpcd, row.RedQty, begin, secnKacd);
- await db.DerivRedemption.AddAsync(created, ct);
- inserted++;
- }
- await db.SaveChangesAsync(ct);
- return (inserted, deleted);
- }
- /// <summary>getBassetUnredScale 결과를 ElsUnredeemedScale 로 upsert — UQ (StdDt, StndBassetCd, SecnTpcd). 요청값 3종 스탬핑. 종목당 1행(첫 행만). 빈결과면 no-op.</summary>
- public static async Task<bool> UpsertUnredeemedScaleAsync(IAppDbContext db, DateOnly stdDt, string stndBassetCd, string secnTpcd, IReadOnlyList<SeibroBassetUnredScaleParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return false;
- }
- var row = rows[0];
- var entity = await db.ElsUnredeemedScale.FirstOrDefaultAsync(c => c.StdDt == stdDt && c.StndBassetCd == stndBassetCd && c.SecnTpcd == secnTpcd, ct);
- if (entity is null)
- {
- await db.ElsUnredeemedScale.AddAsync(ElsUnredeemedScale.Create(stdDt, stndBassetCd, secnTpcd, row.BassetContent, row.UnredSecncnt, row.UnredRema), ct);
- }
- else
- {
- entity.Update(row.BassetContent, row.UnredSecncnt, row.UnredRema);
- }
- await db.SaveChangesAsync(ct);
- return true;
- }
- }
|