| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- namespace Domain.Entities.Paper;
- /// <summary>
- /// 모의투자 보유 포지션 — (AccountID, StockCode) UNIQUE. StockCode 는 FK 없이 denorm 보관 (d0 §2.1).
- /// ReservedQuantity 로 이중 매도를 차단한다.
- /// </summary>
- public class PaperPosition
- {
- [ForeignKey(nameof(AccountID))]
- public virtual PaperAccount Account { get; private set; } = null!;
- [Key]
- public int ID { get; private set; }
- public int AccountID { get; private set; }
- /// <summary>단축코드 (char 6)</summary>
- public string StockCode { get; private set; } = default!;
- /// <summary>보유 수량</summary>
- public int Quantity { get; private set; }
- /// <summary>매도 주문으로 예약된 수량</summary>
- public int ReservedQuantity { get; private set; }
- /// <summary>평균 단가 (18,4) — 매수 체결 시 평균단가법 갱신</summary>
- public decimal AvgPrice { get; private set; }
- public DateTime UpdatedAt { get; private set; } = DateTime.UtcNow;
- [Timestamp]
- public byte[] RowVersion { get; private set; } = default!;
- private PaperPosition() { }
- public static PaperPosition Create(int accountID, string stockCode, int quantity, decimal avgPrice)
- {
- if (accountID <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(accountID));
- }
- if (stockCode is not { Length: 6 })
- {
- throw new ArgumentException("stockCode must be 6 chars", nameof(stockCode));
- }
- if (quantity < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(quantity));
- }
- return new PaperPosition
- {
- AccountID = accountID,
- StockCode = stockCode,
- Quantity = quantity,
- ReservedQuantity = 0,
- AvgPrice = avgPrice
- };
- }
- /// <summary>매도 주문 접수 시 수량 예약 — 가용 수량(Quantity - ReservedQuantity) 부족 시 실패.</summary>
- public void ReserveSell(int quantity)
- {
- if (quantity <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(quantity));
- }
- if (Quantity - ReservedQuantity < quantity)
- {
- throw new InvalidOperationException("매도 가능한 수량이 부족합니다.");
- }
- ReservedQuantity += quantity;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>매도 예약 환원 (주문 취소·거부).</summary>
- public void ReleaseSellReserve(int quantity)
- {
- if (quantity <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(quantity));
- }
- if (ReservedQuantity < quantity)
- {
- throw new InvalidOperationException("환원할 예약 수량이 부족합니다.");
- }
- ReservedQuantity -= quantity;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// 매수 체결 반영 (배치 M2) — 평균단가법으로 AvgPrice 갱신 (순수 체결가 기준, 수수료 제외).
- /// AvgPrice = (AvgPrice × Quantity + fillPrice × qty) / (Quantity + qty).
- /// </summary>
- public void ApplyBuy(int qty, decimal fillPrice)
- {
- if (qty <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(qty));
- }
- if (fillPrice <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(fillPrice));
- }
- var newQty = Quantity + qty;
- AvgPrice = (AvgPrice * Quantity + fillPrice * qty) / newQty;
- Quantity = newQty;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>매도 체결 반영 (배치 M2) — 예약 수량과 보유 수량을 함께 차감한다.</summary>
- public void ApplySell(int qty)
- {
- if (qty <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(qty));
- }
- if (ReservedQuantity < qty || Quantity < qty)
- {
- throw new InvalidOperationException("체결할 예약/보유 수량이 부족합니다.");
- }
- Quantity -= qty;
- ReservedQuantity -= qty;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// 상장폐지 강제 청산 (배치 M2) — 예약 여부와 무관하게 보유 전량을 청산한다.
- /// 반환값은 청산된 수량. 예약 수량도 함께 0 으로 정리한다.
- /// </summary>
- public int ForceLiquidate()
- {
- var liquidated = Quantity;
- Quantity = 0;
- ReservedQuantity = 0;
- UpdatedAt = DateTime.UtcNow;
- return liquidated;
- }
- }
|