| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- using Application.Abstractions.Data;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Helpers;
- /// <summary>
- /// 모의투자 평가값 계산 — 최근 일별 종가(StockDailyPrice)로 포지션·Equity·nav 를 산출한다 (d4 §③).
- /// Position.StockCode(char6) → Stock.ID → 최근 StockDailyPrice.Close 로 평가한다.
- /// </summary>
- public static class PaperValuation
- {
- /// <summary>지정 종목코드들의 최근 종가 맵 (없는 코드는 제외).</summary>
- public static async Task<Dictionary<string, decimal>> GetLatestClosesAsync(
- IAppDbContext db,
- IReadOnlyCollection<string> stockCodes,
- CancellationToken ct)
- {
- if (stockCodes.Count == 0)
- {
- return [];
- }
- // 코드 → 종목 ID
- var idByCode = await db.Stock.AsNoTracking()
- .Where(c => stockCodes.Contains(c.Code))
- .Select(c => new { c.Code, c.ID })
- .ToListAsync(ct);
- var result = new Dictionary<string, decimal>();
- foreach (var s in idByCode)
- {
- var close = await db.StockDailyPrice.AsNoTracking()
- .Where(c => c.StockID == s.ID)
- .OrderByDescending(c => c.TradingDate)
- .Select(c => (int?)c.Close)
- .FirstOrDefaultAsync(ct);
- if (close.HasValue)
- {
- result[s.Code] = close.Value;
- }
- }
- return result;
- }
- /// <summary>단일 종목코드의 최근 종가 (없으면 null).</summary>
- public static async Task<decimal?> GetLatestCloseAsync(IAppDbContext db, string stockCode, CancellationToken ct)
- {
- var stockID = await db.Stock.AsNoTracking()
- .Where(c => c.Code == stockCode)
- .Select(c => (int?)c.ID)
- .FirstOrDefaultAsync(ct);
- if (stockID is null)
- {
- return null;
- }
- var close = await db.StockDailyPrice.AsNoTracking()
- .Where(c => c.StockID == stockID.Value)
- .OrderByDescending(c => c.TradingDate)
- .Select(c => (int?)c.Close)
- .FirstOrDefaultAsync(ct);
- return close;
- }
- /// <summary>
- /// 계좌 평가자산 Equity = Token + ReservedToken + Σ(포지션 수량 × 최근 종가).
- /// 시세 없는 포지션은 0 으로 평가(보수적).
- /// </summary>
- public static decimal ComputeEquity(
- decimal token,
- decimal reservedToken,
- IEnumerable<(string StockCode, int Quantity)> positions,
- IReadOnlyDictionary<string, decimal> latestCloses)
- {
- var positionsValue = 0m;
- foreach (var p in positions)
- {
- if (latestCloses.TryGetValue(p.StockCode, out var close))
- {
- positionsValue += close * p.Quantity;
- }
- }
- return token + reservedToken + positionsValue;
- }
- /// <summary>좌당 순자산 nav = Units > 0 ? Equity / Units : 1.</summary>
- public static decimal ComputeNav(decimal equity, decimal units)
- {
- return units > 0 ? equity / units : 1m;
- }
- }
|