MarketQuoteSnapshot.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. using System.ComponentModel.DataAnnotations;
  2. using Domain.Entities.Stocks.ValueObject;
  3. namespace Domain.Entities.Stocks;
  4. /// <summary>
  5. /// 주요 종목·상품 시세 최신 스냅샷 (주요종목현황·상품선물 그리드). Symbol UNIQUE — 심볼당 1행을 하루 1회 upsert.
  6. /// WorldIndexSnapshot 과 동일 규약(외부 소스 Stooq, PrevClose 로 ChangeVal·FlucRateBp 산출, 등락률 %×100 Bp) + Volume·Category·GroupCode 확장.
  7. /// GroupCode 는 프론트 탭/섹션 키(예: "us-major", "asia-major", "commodity"). Category 는 그리드 렌더 분기(주식=거래량 표시 등).
  8. /// </summary>
  9. public class MarketQuoteSnapshot
  10. {
  11. [Key]
  12. public long ID { get; private set; }
  13. /// <summary>외부 소스(Stooq) 심볼 — 예: "aapl.us" (소문자 정규화 보관)</summary>
  14. public string Symbol { get; private set; } = default!;
  15. /// <summary>표시명 — 예: "Apple"</summary>
  16. public string Name { get; private set; } = default!;
  17. /// <summary>시세 카테고리 (주식/상품/선물/환율/국채수익률)</summary>
  18. public QuoteCategory Category { get; private set; }
  19. /// <summary>프론트 그룹 키 — 예: "us-major", "asia-major", "commodity"</summary>
  20. public string GroupCode { get; private set; } = default!;
  21. /// <summary>국가 코드 (ISO2, 대문자). 상품·선물 등 국가 무관은 빈 문자열.</summary>
  22. public string CountryCode { get; private set; } = default!;
  23. /// <summary>거래일 (소스 최신 종가 기준일)</summary>
  24. public DateOnly TradeDate { get; private set; }
  25. /// <summary>종가</summary>
  26. public decimal Close { get; private set; }
  27. /// <summary>직전 거래일 종가 — ChangeVal 산출 기준 (첫 수집 시 Close 와 동일 → 등락 0)</summary>
  28. public decimal PrevClose { get; private set; }
  29. /// <summary>전일 대비 (Close - PrevClose)</summary>
  30. public decimal ChangeVal { get; private set; }
  31. /// <summary>등락률 basis point = %×100</summary>
  32. public int FlucRateBp { get; private set; }
  33. /// <summary>시가</summary>
  34. public decimal Open { get; private set; }
  35. /// <summary>고가</summary>
  36. public decimal High { get; private set; }
  37. /// <summary>저가</summary>
  38. public decimal Low { get; private set; }
  39. /// <summary>거래량 (결측 시 0)</summary>
  40. public long Volume { get; private set; }
  41. public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow;
  42. private MarketQuoteSnapshot() { }
  43. public static MarketQuoteSnapshot Create(
  44. string symbol,
  45. string name,
  46. QuoteCategory category,
  47. string groupCode,
  48. string countryCode,
  49. DateOnly tradeDate,
  50. decimal close,
  51. decimal open,
  52. decimal high,
  53. decimal low,
  54. long volume
  55. ) {
  56. if (string.IsNullOrWhiteSpace(symbol))
  57. {
  58. throw new ArgumentException("symbol required", nameof(symbol));
  59. }
  60. return new MarketQuoteSnapshot
  61. {
  62. Symbol = symbol.Trim().ToLowerInvariant(),
  63. Name = name.Trim(),
  64. Category = category,
  65. GroupCode = (groupCode ?? string.Empty).Trim().ToLowerInvariant(),
  66. CountryCode = (countryCode ?? string.Empty).Trim().ToUpperInvariant(),
  67. TradeDate = tradeDate,
  68. Close = close,
  69. PrevClose = close, // 첫 수집 — 직전값 없음 → 등락 0
  70. ChangeVal = 0m,
  71. FlucRateBp = 0,
  72. Open = open,
  73. High = high,
  74. Low = low,
  75. Volume = volume,
  76. UpdatedAt = DateTime.UtcNow
  77. };
  78. }
  79. /// <summary>메타(이름·카테고리·그룹·국가) 최신화 — config 변경 반영</summary>
  80. public void UpdateMeta(string name, QuoteCategory category, string groupCode, string countryCode)
  81. {
  82. Name = name.Trim();
  83. Category = category;
  84. GroupCode = (groupCode ?? string.Empty).Trim().ToLowerInvariant();
  85. CountryCode = (countryCode ?? string.Empty).Trim().ToUpperInvariant();
  86. }
  87. /// <summary>
  88. /// 새 시세 적용 (upsert). 거래일이 진행되면 직전 종가를 PrevClose 로 승계하고, 같은/이전 거래일 재수집이면 PrevClose 유지(재실행 안전).
  89. /// 어느 경우든 ChangeVal·FlucRateBp 는 (Close - PrevClose) 로 재산출한다.
  90. /// </summary>
  91. public void Apply(DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low, long volume)
  92. {
  93. if (tradeDate > TradeDate)
  94. {
  95. PrevClose = Close;
  96. TradeDate = tradeDate;
  97. }
  98. Close = close;
  99. Open = open;
  100. High = high;
  101. Low = low;
  102. Volume = volume;
  103. ChangeVal = Close - PrevClose;
  104. FlucRateBp = PrevClose != 0m ? (int)Math.Round((Close - PrevClose) / PrevClose * 10000m, MidpointRounding.AwayFromZero) : 0;
  105. UpdatedAt = DateTime.UtcNow;
  106. }
  107. }