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;
}
}