PaperValuation.cs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. using Application.Abstractions.Data;
  2. using Microsoft.EntityFrameworkCore;
  3. namespace Application.Helpers;
  4. /// <summary>
  5. /// 모의투자 평가값 계산 — 최근 일별 종가(StockDailyPrice)로 포지션·Equity·nav 를 산출한다 (d4 §③).
  6. /// Position.StockCode(char6) → Stock.ID → 최근 StockDailyPrice.Close 로 평가한다.
  7. /// </summary>
  8. public static class PaperValuation
  9. {
  10. /// <summary>지정 종목코드들의 최근 종가 맵 (없는 코드는 제외).</summary>
  11. public static async Task<Dictionary<string, decimal>> GetLatestClosesAsync(
  12. IAppDbContext db,
  13. IReadOnlyCollection<string> stockCodes,
  14. CancellationToken ct)
  15. {
  16. if (stockCodes.Count == 0)
  17. {
  18. return [];
  19. }
  20. // 코드 → 종목 ID
  21. var idByCode = await db.Stock.AsNoTracking()
  22. .Where(c => stockCodes.Contains(c.Code))
  23. .Select(c => new { c.Code, c.ID })
  24. .ToListAsync(ct);
  25. var result = new Dictionary<string, decimal>();
  26. foreach (var s in idByCode)
  27. {
  28. var close = await db.StockDailyPrice.AsNoTracking()
  29. .Where(c => c.StockID == s.ID)
  30. .OrderByDescending(c => c.TradingDate)
  31. .Select(c => (int?)c.Close)
  32. .FirstOrDefaultAsync(ct);
  33. if (close.HasValue)
  34. {
  35. result[s.Code] = close.Value;
  36. }
  37. }
  38. return result;
  39. }
  40. /// <summary>단일 종목코드의 최근 종가 (없으면 null).</summary>
  41. public static async Task<decimal?> GetLatestCloseAsync(IAppDbContext db, string stockCode, CancellationToken ct)
  42. {
  43. var stockID = await db.Stock.AsNoTracking()
  44. .Where(c => c.Code == stockCode)
  45. .Select(c => (int?)c.ID)
  46. .FirstOrDefaultAsync(ct);
  47. if (stockID is null)
  48. {
  49. return null;
  50. }
  51. var close = await db.StockDailyPrice.AsNoTracking()
  52. .Where(c => c.StockID == stockID.Value)
  53. .OrderByDescending(c => c.TradingDate)
  54. .Select(c => (int?)c.Close)
  55. .FirstOrDefaultAsync(ct);
  56. return close;
  57. }
  58. /// <summary>
  59. /// 계좌 평가자산 Equity = Token + ReservedToken + Σ(포지션 수량 × 최근 종가).
  60. /// 시세 없는 포지션은 0 으로 평가(보수적).
  61. /// </summary>
  62. public static decimal ComputeEquity(
  63. decimal token,
  64. decimal reservedToken,
  65. IEnumerable<(string StockCode, int Quantity)> positions,
  66. IReadOnlyDictionary<string, decimal> latestCloses)
  67. {
  68. var positionsValue = 0m;
  69. foreach (var p in positions)
  70. {
  71. if (latestCloses.TryGetValue(p.StockCode, out var close))
  72. {
  73. positionsValue += close * p.Quantity;
  74. }
  75. }
  76. return token + reservedToken + positionsValue;
  77. }
  78. /// <summary>좌당 순자산 nav = Units &gt; 0 ? Equity / Units : 1.</summary>
  79. public static decimal ComputeNav(decimal equity, decimal units)
  80. {
  81. return units > 0 ? equity / units : 1m;
  82. }
  83. }