using System.ComponentModel.DataAnnotations;
using Domain.Entities.Stocks.ValueObject;
namespace Domain.Entities.Stocks;
///
/// 종목 마스터 (KRX 상장종목) — D1 유일 원천.
/// 타 도메인(D2/D4)은 char(6) Code 를 FK 없이 denorm 보관하고 쓰기 시 AnyAsync 사전 검증한다 (d0 §2.1).
/// LastTradingDate~MarketCap 은 목록/quotes 조인 회피용 denorm — 일별 시세 배치가 갱신.
///
public class Stock
{
[Key]
public int ID { get; private set; }
/// 단축코드 (char 6, 예: "005930") — UNIQUE
public string Code { get; private set; } = default!;
/// 표준코드 ISIN (char 12) — 미제공 시 null
public string? ISIN { get; private set; }
/// 종목명 (예: "삼성전자")
public string Name { get; private set; } = default!;
/// 영문 종목명 (선택)
public string? EnglishName { get; private set; }
/// 시장 구분 (1=KOSPI, 2=KOSDAQ, 3=KONEX)
public StockMarket Market { get; private set; }
/// 거래 상태 (0=정상, 1=거래정지, 2=관리종목)
public TradingStatus TradingStatus { get; private set; }
/// DART 고유번호 (char 8) — corpCode 매핑, 미매핑 null (매핑 배치는 M2)
public string? CorpCode { get; private set; }
/// KRX 업종명 (선택)
public string? SectorName { get; private set; }
/// 상장 여부 — 상폐 시 soft-off
public bool IsActive { get; private set; } = true;
/// 상장일 — 원천 API 미제공으로 최초 수집 기준일(basDt) 근사값
public DateOnly ListedDate { get; private set; }
/// 상장폐지일 (상폐 시)
public DateOnly? DelistedDate { get; private set; }
/// 최근 거래일 (denorm)
public DateOnly? LastTradingDate { get; private set; }
/// 최근 종가 (denorm)
public int? LastClosePrice { get; private set; }
/// 최근 등락률 % (denorm)
public decimal? LastChangeRate { get; private set; }
/// 시가총액 (denorm)
public long? MarketCap { get; private set; }
/// SEIBro 발행회사 고객번호 (ISSUCO_CUSTNO) — 기업정보 API 전체의 조인키. 미매핑 null (getShotnByMart 스탬핑)
public int? IssucoCustno { get; private set; }
/// 액면가 (SEIBro getStkStatInfo PVAL) — 미보강 null
public decimal? FaceValue { get; private set; }
/// 1주당 의결권수 (SEIBro getStkStatInfo VOTRNO) — 미보강 null
public decimal? VotingRights { get; private set; }
/// 전자증권 여부 (SEIBro getStkStatInfo ELTSC_YN, Y=전자/N=예탁) — 미보강 null
public bool? IsElectronicSecurity { get; private set; }
/// 총발행주식수 (SEIBro getStkStatInfo TOTAL_STK_CNT) — 미보강 null
public long? TotalIssuedShares { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
private Stock() { }
public static Stock Create(string code, string name, StockMarket market, DateOnly listedDate, string? isin = null, string? corpCode = null, string? englishName = null, string? sectorName = null)
{
if (code is not { Length: 6 } || code.Any(c => !char.IsAsciiDigit(c)))
{
throw new ArgumentException("code must be 6 digits", nameof(code));
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("name required", nameof(name));
}
if (!Enum.IsDefined(market))
{
throw new ArgumentOutOfRangeException(nameof(market));
}
return new Stock
{
Code = code,
Name = name.Trim(),
Market = market,
TradingStatus = TradingStatus.Normal,
ListedDate = listedDate,
ISIN = string.IsNullOrWhiteSpace(isin) ? null : isin.Trim(),
CorpCode = string.IsNullOrWhiteSpace(corpCode) ? null : corpCode.Trim(),
EnglishName = string.IsNullOrWhiteSpace(englishName) ? null : englishName.Trim(),
SectorName = string.IsNullOrWhiteSpace(sectorName) ? null : sectorName.Trim()
};
}
/// 마스터 동기화 — 이름/시장/ISIN(+영문명/업종) 반영. 상폐 상태였다면 재상장으로 복구. englishName/sectorName 은 null 전달 시 미변경.
public void UpdateMaster(string name, StockMarket market, string? isin, string? englishName = null, string? sectorName = null)
{
var changed = false;
if (!string.IsNullOrWhiteSpace(name) && Name != name.Trim())
{
Name = name.Trim();
changed = true;
}
if (Enum.IsDefined(market) && Market != market)
{
Market = market;
changed = true;
}
var normalizedIsin = string.IsNullOrWhiteSpace(isin) ? null : isin.Trim();
if (normalizedIsin is not null && ISIN != normalizedIsin)
{
ISIN = normalizedIsin;
changed = true;
}
var normalizedEnglishName = string.IsNullOrWhiteSpace(englishName) ? null : englishName.Trim();
if (normalizedEnglishName is not null && EnglishName != normalizedEnglishName)
{
EnglishName = normalizedEnglishName;
changed = true;
}
var normalizedSectorName = string.IsNullOrWhiteSpace(sectorName) ? null : sectorName.Trim();
if (normalizedSectorName is not null && SectorName != normalizedSectorName)
{
SectorName = normalizedSectorName;
changed = true;
}
if (!IsActive)
{
IsActive = true;
DelistedDate = null;
changed = true;
}
if (changed)
{
UpdatedAt = DateTime.UtcNow;
}
}
/// 상장폐지 — soft-off (row 는 보존, 과거 시세/참조 유지)
public void MarkDelisted(DateOnly date)
{
if (!IsActive)
{
return;
}
IsActive = false;
DelistedDate = date;
UpdatedAt = DateTime.UtcNow;
}
public void SetTradingStatus(TradingStatus status)
{
if (!Enum.IsDefined(status) || TradingStatus == status)
{
return;
}
TradingStatus = status;
UpdatedAt = DateTime.UtcNow;
}
public void SetCorpCode(string? corpCode)
{
var normalized = string.IsNullOrWhiteSpace(corpCode) ? null : corpCode.Trim();
if (CorpCode == normalized)
{
return;
}
CorpCode = normalized;
UpdatedAt = DateTime.UtcNow;
}
/// SEIBro 발행회사번호 스탬핑 (getShotnByMart Code=ShortCode 조인) — null 전달·동일 값이면 무변경
public void SetIssucoCustno(int? issucoCustno)
{
if (issucoCustno is null || IssucoCustno == issucoCustno)
{
return;
}
IssucoCustno = issucoCustno;
UpdatedAt = DateTime.UtcNow;
}
/// SEIBro 종목 정보(getStkStatInfo) 보강 — 액면가/의결권/총발행주식수/전자증권 여부(+발행회사번호). null 전달 필드는 미변경.
public void EnrichFromSeibro(int? issucoCustno, decimal? faceValue, decimal? votingRights, long? totalIssuedShares, bool? isElectronicSecurity)
{
var changed = false;
if (issucoCustno is not null && IssucoCustno != issucoCustno)
{
IssucoCustno = issucoCustno;
changed = true;
}
if (faceValue is not null && FaceValue != faceValue)
{
FaceValue = faceValue;
changed = true;
}
if (votingRights is not null && VotingRights != votingRights)
{
VotingRights = votingRights;
changed = true;
}
if (totalIssuedShares is not null && TotalIssuedShares != totalIssuedShares)
{
TotalIssuedShares = totalIssuedShares;
changed = true;
}
if (isElectronicSecurity is not null && IsElectronicSecurity != isElectronicSecurity)
{
IsElectronicSecurity = isElectronicSecurity;
changed = true;
}
if (changed)
{
UpdatedAt = DateTime.UtcNow;
}
}
/// 일별 시세 배치가 최근 시세 denorm 을 갱신. 과거 일자 재수집은 무시.
public void UpdateLastPrice(DateOnly tradingDate, int closePrice, decimal changeRate, long? marketCap)
{
if (LastTradingDate.HasValue && tradingDate < LastTradingDate.Value)
{
return;
}
LastTradingDate = tradingDate;
LastClosePrice = closePrice;
LastChangeRate = changeRate;
MarketCap = marketCap;
UpdatedAt = DateTime.UtcNow;
}
}