WorldIndexSnapshot.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. using System.ComponentModel.DataAnnotations;
  2. namespace Domain.Entities.Stocks;
  3. /// <summary>
  4. /// 세계 주요국 증시지수 최신 스냅샷 (메인 세계지도 핀). Symbol UNIQUE — 심볼당 1행을 하루 1회 upsert 한다.
  5. /// 외부 소스(Stooq)는 전일 대비/등락률을 직접 주지 않으므로, 직전 저장 종가(PrevClose)로부터 ChangeVal·FlucRateBp 를 산출한다.
  6. /// 등락률은 정수 보관을 위해 %×100 basis point(Bp)로 저장 (예: -0.70% → -70). IndexDailyPrice 규약과 동일.
  7. /// </summary>
  8. public class WorldIndexSnapshot
  9. {
  10. [Key]
  11. public long ID { get; private set; }
  12. /// <summary>외부 소스(Stooq) 심볼 — 예: "^spx" (소문자 정규화 보관)</summary>
  13. public string Symbol { get; private set; } = default!;
  14. /// <summary>지수 표시명 — 예: "S&amp;P 500"</summary>
  15. public string Name { get; private set; } = default!;
  16. /// <summary>국가 코드 (ISO2, 대문자) — 프론트 좌표맵 키</summary>
  17. public string CountryCode { get; private set; } = default!;
  18. /// <summary>거래소·시장 표기 — 예: "NYSE·Nasdaq"</summary>
  19. public string ExchangeName { get; private set; } = default!;
  20. /// <summary>거래일 (소스 최신 종가 기준일)</summary>
  21. public DateOnly TradeDate { get; private set; }
  22. /// <summary>종가</summary>
  23. public decimal Close { get; private set; }
  24. /// <summary>직전 거래일 종가 — ChangeVal 산출 기준 (첫 수집 시 Close 와 동일 → 등락 0)</summary>
  25. public decimal PrevClose { get; private set; }
  26. /// <summary>전일 대비 (Close - PrevClose)</summary>
  27. public decimal ChangeVal { get; private set; }
  28. /// <summary>등락률 basis point = %×100</summary>
  29. public int FlucRateBp { get; private set; }
  30. /// <summary>시가</summary>
  31. public decimal Open { get; private set; }
  32. /// <summary>고가</summary>
  33. public decimal High { get; private set; }
  34. /// <summary>저가</summary>
  35. public decimal Low { get; private set; }
  36. /// <summary>최근 종가 스파크라인 — JSON decimal 배열(과거→최신, 예: "[7250.1,7260.3,...]"). 수집 소스 시계열에서 생성, 없으면 null.</summary>
  37. public string? Spark { get; private set; }
  38. public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow;
  39. private WorldIndexSnapshot() { }
  40. public static WorldIndexSnapshot Create(
  41. string symbol,
  42. string name,
  43. string countryCode,
  44. string exchangeName,
  45. DateOnly tradeDate,
  46. decimal close,
  47. decimal open,
  48. decimal high,
  49. decimal low
  50. ) {
  51. if (string.IsNullOrWhiteSpace(symbol))
  52. {
  53. throw new ArgumentException("symbol required", nameof(symbol));
  54. }
  55. if (string.IsNullOrWhiteSpace(countryCode))
  56. {
  57. throw new ArgumentException("countryCode required", nameof(countryCode));
  58. }
  59. return new WorldIndexSnapshot
  60. {
  61. Symbol = symbol.Trim().ToLowerInvariant(),
  62. Name = name.Trim(),
  63. CountryCode = countryCode.Trim().ToUpperInvariant(),
  64. ExchangeName = exchangeName.Trim(),
  65. TradeDate = tradeDate,
  66. Close = close,
  67. PrevClose = close, // 첫 수집 — 직전값 없음 → 등락 0
  68. ChangeVal = 0m,
  69. FlucRateBp = 0,
  70. Open = open,
  71. High = high,
  72. Low = low,
  73. UpdatedAt = DateTime.UtcNow
  74. };
  75. }
  76. /// <summary>메타(이름·국가·거래소) 최신화 — config 변경 반영</summary>
  77. public void UpdateMeta(string name, string countryCode, string exchangeName)
  78. {
  79. Name = name.Trim();
  80. CountryCode = countryCode.Trim().ToUpperInvariant();
  81. ExchangeName = exchangeName.Trim();
  82. }
  83. /// <summary>
  84. /// 새 시세 적용 (upsert). 거래일이 진행되면 직전 종가를 PrevClose 로 승계하고, 같은/이전 거래일 재수집이면 PrevClose 를 유지(재실행 안전).
  85. /// 어느 경우든 ChangeVal·FlucRateBp 는 (Close - PrevClose) 로 재산출한다.
  86. /// </summary>
  87. public void Apply(DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low)
  88. {
  89. if (tradeDate > TradeDate)
  90. {
  91. PrevClose = Close;
  92. TradeDate = tradeDate;
  93. }
  94. Close = close;
  95. Open = open;
  96. High = high;
  97. Low = low;
  98. ChangeVal = Close - PrevClose;
  99. FlucRateBp = PrevClose != 0m ? (int)Math.Round((Close - PrevClose) / PrevClose * 10000m, MidpointRounding.AwayFromZero) : 0;
  100. UpdatedAt = DateTime.UtcNow;
  101. }
  102. /// <summary>스파크라인 종가 시계열(JSON) 최신화. null/빈값이면 미변경(수집 실패 시 기존 값 보존).</summary>
  103. public void SetSpark(string? sparkJson)
  104. {
  105. if (!string.IsNullOrWhiteSpace(sparkJson))
  106. {
  107. Spark = sparkJson;
  108. }
  109. }
  110. }