| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- using System.ComponentModel.DataAnnotations;
- using Domain.Entities.Stocks.ValueObject;
- namespace Domain.Entities.Stocks;
- /// <summary>
- /// 종목 마스터 (KRX 상장종목) — D1 유일 원천.
- /// 타 도메인(D2/D4)은 char(6) Code 를 FK 없이 denorm 보관하고 쓰기 시 AnyAsync 사전 검증한다 (d0 §2.1).
- /// LastTradingDate~MarketCap 은 목록/quotes 조인 회피용 denorm — 일별 시세 배치가 갱신.
- /// </summary>
- public class Stock
- {
- [Key]
- public int ID { get; private set; }
- /// <summary>단축코드 (char 6, 예: "005930") — UNIQUE</summary>
- public string Code { get; private set; } = default!;
- /// <summary>표준코드 ISIN (char 12) — 미제공 시 null</summary>
- public string? ISIN { get; private set; }
- /// <summary>종목명 (예: "삼성전자")</summary>
- public string Name { get; private set; } = default!;
- /// <summary>영문 종목명 (선택)</summary>
- public string? EnglishName { get; private set; }
- /// <summary>시장 구분 (1=KOSPI, 2=KOSDAQ, 3=KONEX)</summary>
- public StockMarket Market { get; private set; }
- /// <summary>거래 상태 (0=정상, 1=거래정지, 2=관리종목)</summary>
- public TradingStatus TradingStatus { get; private set; }
- /// <summary>DART 고유번호 (char 8) — corpCode 매핑, 미매핑 null (매핑 배치는 M2)</summary>
- public string? CorpCode { get; private set; }
- /// <summary>KRX 업종명 (선택)</summary>
- public string? SectorName { get; private set; }
- /// <summary>상장 여부 — 상폐 시 soft-off</summary>
- public bool IsActive { get; private set; } = true;
- /// <summary>상장일 — 원천 API 미제공으로 최초 수집 기준일(basDt) 근사값</summary>
- public DateOnly ListedDate { get; private set; }
- /// <summary>상장폐지일 (상폐 시)</summary>
- public DateOnly? DelistedDate { get; private set; }
- /// <summary>최근 거래일 (denorm)</summary>
- public DateOnly? LastTradingDate { get; private set; }
- /// <summary>최근 종가 (denorm)</summary>
- public int? LastClosePrice { get; private set; }
- /// <summary>최근 등락률 % (denorm)</summary>
- public decimal? LastChangeRate { get; private set; }
- /// <summary>시가총액 (denorm)</summary>
- public long? MarketCap { 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()
- };
- }
- /// <summary>마스터 동기화 — 이름/시장/ISIN(+영문명/업종) 반영. 상폐 상태였다면 재상장으로 복구. englishName/sectorName 은 null 전달 시 미변경.</summary>
- 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;
- }
- }
- /// <summary>상장폐지 — soft-off (row 는 보존, 과거 시세/참조 유지)</summary>
- 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;
- }
- /// <summary>일별 시세 배치가 최근 시세 denorm 을 갱신. 과거 일자 재수집은 무시.</summary>
- 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;
- }
- }
|