using Application.Abstractions.Data;
using Microsoft.EntityFrameworkCore;
namespace Application.Helpers;
///
/// 모의투자 평가값 계산 — 최근 일별 종가(StockDailyPrice)로 포지션·Equity·nav 를 산출한다 (d4 §③).
/// Position.StockCode(char6) → Stock.ID → 최근 StockDailyPrice.Close 로 평가한다.
///
public static class PaperValuation
{
/// 지정 종목코드들의 최근 종가 맵 (없는 코드는 제외).
public static async Task> GetLatestClosesAsync(
IAppDbContext db,
IReadOnlyCollection 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();
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;
}
/// 단일 종목코드의 최근 종가 (없으면 null).
public static async Task 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;
}
///
/// 계좌 평가자산 Equity = Token + ReservedToken + Σ(포지션 수량 × 최근 종가).
/// 시세 없는 포지션은 0 으로 평가(보수적).
///
public static decimal ComputeEquity(
decimal token,
decimal reservedToken,
IEnumerable<(string StockCode, int Quantity)> positions,
IReadOnlyDictionary 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;
}
/// 좌당 순자산 nav = Units > 0 ? Equity / Units : 1.
public static decimal ComputeNav(decimal equity, decimal units)
{
return units > 0 ? equity / units : 1m;
}
}