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