| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using Domain.Entities.Forum.Posts.ValueObject;
- using Domain.Entities.Members;
- namespace Domain.Entities.Forum.Posts;
- /// <summary>
- /// 예측글 (d2 §③) — 종목 방향/기간 예측을 작성 시점에 확정 기록하고, 채점 배치(M4)가 상태를 확정한다.
- /// 작성 시 BasePrice(작성 시점 최신 T+1 종가)와 DueDate(영업일 + HorizonDays)를 스냅샷으로 고정한다.
- /// 예측글이 삭제(IsDeleted)돼도 Prediction 은 유지·채점된다(삭제 회피 어뷰징 차단).
- /// Stock FK 는 걸지 않는다(D1 소유). PostID 는 1:1(UQ).
- /// M2 는 <b>Pending 생성만</b> — SettledPrice/SettledAt/상태 확정은 M4 PredictionSettlementService 소관.
- /// </summary>
- public class PostPrediction
- {
- [ForeignKey(nameof(PostID))]
- public virtual Post Post { get; private set; } = null!;
- [ForeignKey(nameof(MemberID))]
- public virtual Member Member { get; private set; } = null!;
- [Key]
- public int ID { get; private set; }
- /// <summary>예측이 속한 게시글 (1:1)</summary>
- public int PostID { get; private set; }
- /// <summary>작성 회원</summary>
- public int MemberID { get; private set; }
- /// <summary>예측 종목 단축코드 (char 6)</summary>
- public string StockCode { get; private set; } = default!;
- /// <summary>예측 방향 (1=상승, 2=하락)</summary>
- public PredictionDirection Direction { get; private set; }
- /// <summary>작성 시점 기준가 (최신 T+1 종가 스냅샷)</summary>
- public decimal BasePrice { get; private set; }
- /// <summary>목표가 (선택) — 지정 시 도달 여부로 채점</summary>
- public decimal? TargetPrice { get; private set; }
- /// <summary>예측 기간 (영업일 수: 5 또는 20)</summary>
- public short HorizonDays { get; private set; }
- /// <summary>채점 예정일 (작성일 + HorizonDays 영업일)</summary>
- public DateOnly DueDate { get; private set; }
- /// <summary>상태 (0=Pending/1=Hit/2=Miss/3=Void)</summary>
- public PredictionStatus Status { get; private set; } = PredictionStatus.Pending;
- /// <summary>채점 시점 종가 (M4 확정)</summary>
- public decimal? SettledPrice { get; private set; }
- /// <summary>채점 일시 (M4 확정)</summary>
- public DateTime? SettledAt { get; private set; }
- public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
- private PostPrediction() { }
- /// <summary>Pending 예측 생성 (d2 §③ M2). 채점은 M4.</summary>
- public static PostPrediction Create(
- int postID,
- int memberID,
- string stockCode,
- PredictionDirection direction,
- decimal basePrice,
- short horizonDays,
- DateOnly dueDate,
- decimal? targetPrice = null)
- {
- if (postID <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(postID));
- }
- if (memberID <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(memberID));
- }
- if (stockCode is not { Length: 6 } || stockCode.Any(c => !char.IsAsciiDigit(c)))
- {
- throw new ArgumentException("stockCode must be 6 digits", nameof(stockCode));
- }
- if (!Enum.IsDefined(direction))
- {
- throw new ArgumentOutOfRangeException(nameof(direction));
- }
- if (basePrice <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(basePrice));
- }
- if (horizonDays <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(horizonDays));
- }
- return new PostPrediction
- {
- PostID = postID,
- MemberID = memberID,
- StockCode = stockCode,
- Direction = direction,
- BasePrice = basePrice,
- TargetPrice = targetPrice is > 0 ? targetPrice : null,
- HorizonDays = horizonDays,
- DueDate = dueDate,
- Status = PredictionStatus.Pending
- };
- }
- /// <summary>
- /// 채점 확정 (D2 M4 예측 채점+트랙레코드) — Pending 예측을 Hit/Miss/Void 로 확정하고 종가·채점일시를 기록.
- /// Void 는 시세 결측·상폐·거래정지로 종가가 없을 수 있어 settledPrice 를 null 허용한다.
- /// 이미 확정(비 Pending)된 예측은 멱등하게 무시한다 (배치 재실행 안전).
- /// </summary>
- public void Settle(PredictionStatus status, decimal? settledPrice)
- {
- if (Status != PredictionStatus.Pending)
- {
- return;
- }
- if (status == PredictionStatus.Pending)
- {
- throw new ArgumentException("Settle status must be Hit/Miss/Void", nameof(status));
- }
- Status = status;
- SettledPrice = settledPrice;
- SettledAt = DateTime.UtcNow;
- }
- }
|