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