using System.ComponentModel.DataAnnotations; namespace Domain.Entities.Stocks; /// /// 세계 주요국 증시지수 최신 스냅샷 (메인 세계지도 핀). Symbol UNIQUE — 심볼당 1행을 하루 1회 upsert 한다. /// 외부 소스(Stooq)는 전일 대비/등락률을 직접 주지 않으므로, 직전 저장 종가(PrevClose)로부터 ChangeVal·FlucRateBp 를 산출한다. /// 등락률은 정수 보관을 위해 %×100 basis point(Bp)로 저장 (예: -0.70% → -70). IndexDailyPrice 규약과 동일. /// public class WorldIndexSnapshot { [Key] public long ID { get; private set; } /// 외부 소스(Stooq) 심볼 — 예: "^spx" (소문자 정규화 보관) public string Symbol { get; private set; } = default!; /// 지수 표시명 — 예: "S&P 500" public string Name { get; private set; } = default!; /// 국가 코드 (ISO2, 대문자) — 프론트 좌표맵 키 public string CountryCode { get; private set; } = default!; /// 거래소·시장 표기 — 예: "NYSE·Nasdaq" public string ExchangeName { get; private set; } = default!; /// 거래일 (소스 최신 종가 기준일) public DateOnly TradeDate { get; private set; } /// 종가 public decimal Close { get; private set; } /// 직전 거래일 종가 — ChangeVal 산출 기준 (첫 수집 시 Close 와 동일 → 등락 0) public decimal PrevClose { get; private set; } /// 전일 대비 (Close - PrevClose) public decimal ChangeVal { get; private set; } /// 등락률 basis point = %×100 public int FlucRateBp { get; private set; } /// 시가 public decimal Open { get; private set; } /// 고가 public decimal High { get; private set; } /// 저가 public decimal Low { get; private set; } /// 최근 종가 스파크라인 — JSON decimal 배열(과거→최신, 예: "[7250.1,7260.3,...]"). 수집 소스 시계열에서 생성, 없으면 null. public string? Spark { get; private set; } public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow; private WorldIndexSnapshot() { } public static WorldIndexSnapshot Create( string symbol, string name, string countryCode, string exchangeName, DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low ) { if (string.IsNullOrWhiteSpace(symbol)) { throw new ArgumentException("symbol required", nameof(symbol)); } if (string.IsNullOrWhiteSpace(countryCode)) { throw new ArgumentException("countryCode required", nameof(countryCode)); } return new WorldIndexSnapshot { Symbol = symbol.Trim().ToLowerInvariant(), Name = name.Trim(), CountryCode = countryCode.Trim().ToUpperInvariant(), ExchangeName = exchangeName.Trim(), TradeDate = tradeDate, Close = close, PrevClose = close, // 첫 수집 — 직전값 없음 → 등락 0 ChangeVal = 0m, FlucRateBp = 0, Open = open, High = high, Low = low, UpdatedAt = DateTime.UtcNow }; } /// 메타(이름·국가·거래소) 최신화 — config 변경 반영 public void UpdateMeta(string name, string countryCode, string exchangeName) { Name = name.Trim(); CountryCode = countryCode.Trim().ToUpperInvariant(); ExchangeName = exchangeName.Trim(); } /// /// 새 시세 적용 (upsert). 거래일이 진행되면 직전 종가를 PrevClose 로 승계하고, 같은/이전 거래일 재수집이면 PrevClose 를 유지(재실행 안전). /// 어느 경우든 ChangeVal·FlucRateBp 는 (Close - PrevClose) 로 재산출한다. /// public void Apply(DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low) { if (tradeDate > TradeDate) { PrevClose = Close; TradeDate = tradeDate; } Close = close; Open = open; High = high; Low = low; ChangeVal = Close - PrevClose; FlucRateBp = PrevClose != 0m ? (int)Math.Round((Close - PrevClose) / PrevClose * 10000m, MidpointRounding.AwayFromZero) : 0; UpdatedAt = DateTime.UtcNow; } /// 스파크라인 종가 시계열(JSON) 최신화. null/빈값이면 미변경(수집 실패 시 기존 값 보존). public void SetSpark(string? sparkJson) { if (!string.IsNullOrWhiteSpace(sparkJson)) { Spark = sparkJson; } } }