using System.ComponentModel.DataAnnotations; using Domain.Entities.Stocks.ValueObject; namespace Domain.Entities.Stocks; /// /// 주요 종목·상품 시세 최신 스냅샷 (주요종목현황·상품선물 그리드). Symbol UNIQUE — 심볼당 1행을 하루 1회 upsert. /// WorldIndexSnapshot 과 동일 규약(외부 소스 Stooq, PrevClose 로 ChangeVal·FlucRateBp 산출, 등락률 %×100 Bp) + Volume·Category·GroupCode 확장. /// GroupCode 는 프론트 탭/섹션 키(예: "us-major", "asia-major", "commodity"). Category 는 그리드 렌더 분기(주식=거래량 표시 등). /// public class MarketQuoteSnapshot { [Key] public long ID { get; private set; } /// 외부 소스(Stooq) 심볼 — 예: "aapl.us" (소문자 정규화 보관) public string Symbol { get; private set; } = default!; /// 표시명 — 예: "Apple" public string Name { get; private set; } = default!; /// 시세 카테고리 (주식/상품/선물/환율/국채수익률) public QuoteCategory Category { get; private set; } /// 프론트 그룹 키 — 예: "us-major", "asia-major", "commodity" public string GroupCode { get; private set; } = default!; /// 국가 코드 (ISO2, 대문자). 상품·선물 등 국가 무관은 빈 문자열. public string CountryCode { 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; } /// 거래량 (결측 시 0) public long Volume { get; private set; } public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow; private MarketQuoteSnapshot() { } public static MarketQuoteSnapshot Create( string symbol, string name, QuoteCategory category, string groupCode, string countryCode, DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low, long volume ) { if (string.IsNullOrWhiteSpace(symbol)) { throw new ArgumentException("symbol required", nameof(symbol)); } return new MarketQuoteSnapshot { Symbol = symbol.Trim().ToLowerInvariant(), Name = name.Trim(), Category = category, GroupCode = (groupCode ?? string.Empty).Trim().ToLowerInvariant(), CountryCode = (countryCode ?? string.Empty).Trim().ToUpperInvariant(), TradeDate = tradeDate, Close = close, PrevClose = close, // 첫 수집 — 직전값 없음 → 등락 0 ChangeVal = 0m, FlucRateBp = 0, Open = open, High = high, Low = low, Volume = volume, UpdatedAt = DateTime.UtcNow }; } /// 메타(이름·카테고리·그룹·국가) 최신화 — config 변경 반영 public void UpdateMeta(string name, QuoteCategory category, string groupCode, string countryCode) { Name = name.Trim(); Category = category; GroupCode = (groupCode ?? string.Empty).Trim().ToLowerInvariant(); CountryCode = (countryCode ?? string.Empty).Trim().ToUpperInvariant(); } /// /// 새 시세 적용 (upsert). 거래일이 진행되면 직전 종가를 PrevClose 로 승계하고, 같은/이전 거래일 재수집이면 PrevClose 유지(재실행 안전). /// 어느 경우든 ChangeVal·FlucRateBp 는 (Close - PrevClose) 로 재산출한다. /// public void Apply(DateOnly tradeDate, decimal close, decimal open, decimal high, decimal low, long volume) { if (tradeDate > TradeDate) { PrevClose = Close; TradeDate = tradeDate; } Close = close; Open = open; High = high; Low = low; Volume = volume; ChangeVal = Close - PrevClose; FlucRateBp = PrevClose != 0m ? (int)Math.Round((Close - PrevClose) / PrevClose * 10000m, MidpointRounding.AwayFromZero) : 0; UpdatedAt = DateTime.UtcNow; } }