| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 |
- using Application.Abstractions.Data;
- using Application.Helpers;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- namespace Infrastructure.StockData;
- /// <summary>
- /// 모의투자 체결·스냅샷 배치 (d4 M2). KST 13:10 기동 후 직전 영업일(TargetDate) 시세 적재를 폴링하고
- /// (StockDailyPrice 존재 여부, 10분 간격 ~18:00), 적재되면 체결 패스 → 스냅샷 패스를 1회 수행한다.
- ///
- /// DailyPriceSyncService(13:10) 가 시세를 먼저 넣으므로 그 뒤에 자연히 통과한다. 시세 미적재 상태로
- /// 마감 시각을 넘기면 그날은 skip 하고 다음 날 다시 시도한다. BackgroundJobs:PaperFill 플래그로 토글,
- /// Web.Api 전용(중복 실행 방지). 체결은 주문 단위 컨텍스트로 격리(PaperBatchEngine 참조).
- /// </summary>
- internal sealed class PaperFillService(
- IServiceScopeFactory scopeFactory,
- IAppDbContextFactory dbContextFactory,
- ILogger<PaperFillService> logger
- ) : DailyScheduledService(logger)
- {
- /// <summary>폴링 시작 시각(KST) — DailyPriceSync 13:10 직후.</summary>
- private static readonly TimeOnly StartTime = new(13, 10);
- /// <summary>폴링 마감 시각(KST) — 이때까지 시세 미적재면 그날 skip.</summary>
- private static readonly TimeOnly Deadline = new(18, 0);
- /// <summary>시세 적재 폴링 간격.</summary>
- private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(10);
- protected override string JobName => "PaperFill";
- protected override TimeOnly TargetTime => StartTime;
- protected override async Task<bool> RunOnceAsync(DateOnly todayKst, CancellationToken ct)
- {
- // 직전 영업일 = 이번에 적재될 시세의 TargetDate (DailyPriceSyncService 와 동일 기준)
- DateOnly targetDate;
- using (var scope = scopeFactory.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- targetDate = await MarketCalendar.GetPreviousBusinessDayAsync(db, todayKst, ct);
- }
- // 시세 적재 폴링 (10분 간격, 마감 시각까지)
- var loaded = await WaitForPricesAsync(targetDate, ct);
- if (!loaded)
- {
- Logger.LogWarning("[{Job}] targetDate={TargetDate} 시세 미적재 — 마감({Deadline}) 초과, 오늘 skip", JobName, targetDate, Deadline);
- return true; // 다음 날 다시 시도
- }
- // 1) 체결 패스 — 주문 단위 독립 컨텍스트 (동시성 격리 + 멱등)
- var fillResult = await PaperBatchEngine.RunFillPassAsync(dbContextFactory.CreateDbContext, targetDate, ct);
- Logger.LogInformation("[{Job}] 체결 완료 — targetDate={TargetDate}, considered={Considered}, filled={Filled}, rejected={Rejected}, skipped={Skipped}, conflicts={Conflicts}",
- JobName, targetDate, fillResult.Considered, fillResult.Filled, fillResult.Rejected, fillResult.Skipped, fillResult.Conflicts);
- // 2) 스냅샷 패스 — 상폐 청산 + 좌수 NAV/수익률/MDD upsert (단일 컨텍스트)
- using (var scope = scopeFactory.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- var paper = await db.Config.AsNoTracking().OrderByDescending(c => c.ID).Select(c => c.Paper).FirstOrDefaultAsync(ct);
- var feeRateBp = paper?.FeeRateBp ?? 15;
- var taxRateBp = paper?.TaxRateBp ?? 18;
- var snapResult = await PaperBatchEngine.RunSnapshotPassAsync(db, targetDate, feeRateBp, taxRateBp, ct);
- Logger.LogInformation("[{Job}] 스냅샷 완료 — targetDate={TargetDate}, liquidated={Liquidated}, snapshots={Snapshots}",
- JobName, targetDate, snapResult.Liquidated, snapResult.Snapshots);
- }
- return true;
- }
- /// <summary>targetDate 의 StockDailyPrice 가 적재될 때까지 마감 시각(KST)까지 10분 간격 폴링.</summary>
- private async Task<bool> WaitForPricesAsync(DateOnly targetDate, CancellationToken ct)
- {
- while (!ct.IsCancellationRequested)
- {
- using (var scope = scopeFactory.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- if (await db.StockDailyPrice.AsNoTracking().AnyAsync(c => c.TradingDate == targetDate, ct))
- {
- return true;
- }
- }
- if (TimeOnly.FromDateTime(NowKst()) >= Deadline)
- {
- return false;
- }
- Logger.LogInformation("[{Job}] targetDate={TargetDate} 시세 미적재 — {Interval} 후 재확인", JobName, targetDate, PollInterval);
- try
- {
- await Task.Delay(PollInterval, ct);
- }
- catch (OperationCanceledException)
- {
- return false;
- }
- }
- return false;
- }
- }
|