PaperAccount.cs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Members;
  4. namespace Domain.Entities.Paper;
  5. /// <summary>
  6. /// 모의투자 계좌 — 회원당 1개(상시 단일 계좌). 토큰을 입금해 매매, 출금해 회수한다.
  7. /// 좌수제(펀드 NAV): 입출금 왜곡 없이 순수 운용 성과를 측정 (d4 §③ 좌수제).
  8. /// Equity/nav 등 평가값은 핸들러가 시세로 계산 — 엔티티에 저장하지 않는다.
  9. /// </summary>
  10. public class PaperAccount
  11. {
  12. [ForeignKey(nameof(MemberID))]
  13. public virtual Member Member { get; private set; } = null!;
  14. [Key]
  15. public int ID { get; private set; }
  16. public int MemberID { get; private set; }
  17. /// <summary>자유 현금 토큰 (매수 예약 제외)</summary>
  18. public decimal Token { get; private set; }
  19. /// <summary>매수 주문으로 예약된 토큰</summary>
  20. public decimal ReservedToken { get; private set; }
  21. /// <summary>좌수 (18,8) — 발행/소각으로만 변동, 매매는 좌수 불변</summary>
  22. public decimal Units { get; private set; }
  23. /// <summary>누적 입금 토큰</summary>
  24. public decimal TotalDeposited { get; private set; }
  25. /// <summary>누적 출금 토큰</summary>
  26. public decimal TotalWithdrawn { get; private set; }
  27. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  28. [Timestamp]
  29. public byte[] RowVersion { get; private set; } = default!;
  30. private PaperAccount() { }
  31. public static PaperAccount Create(int memberID)
  32. {
  33. if (memberID <= 0)
  34. {
  35. throw new ArgumentOutOfRangeException(nameof(memberID));
  36. }
  37. return new PaperAccount
  38. {
  39. MemberID = memberID,
  40. Token = 0,
  41. ReservedToken = 0,
  42. Units = 0,
  43. TotalDeposited = 0,
  44. TotalWithdrawn = 0
  45. };
  46. }
  47. /// <summary>입금 — 토큰/좌수 증가. 좌수 발행 계산은 호출자(nav 필요).</summary>
  48. public void Deposit(decimal tokenAmount, decimal unitsIssued)
  49. {
  50. if (tokenAmount <= 0)
  51. {
  52. throw new ArgumentOutOfRangeException(nameof(tokenAmount));
  53. }
  54. if (unitsIssued <= 0)
  55. {
  56. throw new ArgumentOutOfRangeException(nameof(unitsIssued));
  57. }
  58. Token += tokenAmount;
  59. Units += unitsIssued;
  60. TotalDeposited += tokenAmount;
  61. }
  62. /// <summary>출금 — 자유 현금 토큰만 인출 가능. 좌수 소각.</summary>
  63. public void Withdraw(decimal tokenAmount, decimal unitsBurned)
  64. {
  65. if (tokenAmount <= 0)
  66. {
  67. throw new ArgumentOutOfRangeException(nameof(tokenAmount));
  68. }
  69. if (Token < tokenAmount)
  70. {
  71. throw new InvalidOperationException("출금 가능한 자유 토큰이 부족합니다.");
  72. }
  73. if (unitsBurned <= 0 || unitsBurned > Units)
  74. {
  75. throw new ArgumentOutOfRangeException(nameof(unitsBurned));
  76. }
  77. Token -= tokenAmount;
  78. Units -= unitsBurned;
  79. TotalWithdrawn += tokenAmount;
  80. }
  81. /// <summary>매수 예약 — 자유 토큰을 예약금으로 이동.</summary>
  82. public void ReserveForBuy(decimal amount)
  83. {
  84. if (amount <= 0)
  85. {
  86. throw new ArgumentOutOfRangeException(nameof(amount));
  87. }
  88. if (Token < amount)
  89. {
  90. throw new InvalidOperationException("매수 예약 가능한 토큰이 부족합니다.");
  91. }
  92. Token -= amount;
  93. ReservedToken += amount;
  94. }
  95. /// <summary>매수 예약 환원 (주문 취소·거부).</summary>
  96. public void ReleaseBuyReserve(decimal amount)
  97. {
  98. if (amount <= 0)
  99. {
  100. throw new ArgumentOutOfRangeException(nameof(amount));
  101. }
  102. if (ReservedToken < amount)
  103. {
  104. throw new InvalidOperationException("환원할 예약금이 부족합니다.");
  105. }
  106. ReservedToken -= amount;
  107. Token += amount;
  108. }
  109. /// <summary>
  110. /// 매수 체결 정산 (배치 M2) — 예약금 전액을 해제하고 실제 사용분(actualCost)을 제외한 잔여를 자유 토큰으로 환급한다.
  111. /// ReservedToken -= reservedAmount; Token += (reservedAmount - actualCost). 좌수는 불변.
  112. /// </summary>
  113. public void SettleBuy(decimal reservedAmount, decimal actualCost)
  114. {
  115. if (reservedAmount <= 0)
  116. {
  117. throw new ArgumentOutOfRangeException(nameof(reservedAmount));
  118. }
  119. if (actualCost < 0 || actualCost > reservedAmount)
  120. {
  121. throw new ArgumentOutOfRangeException(nameof(actualCost));
  122. }
  123. if (ReservedToken < reservedAmount)
  124. {
  125. throw new InvalidOperationException("정산할 예약금이 부족합니다.");
  126. }
  127. ReservedToken -= reservedAmount;
  128. Token += reservedAmount - actualCost;
  129. }
  130. /// <summary>매도 대금·청산 대금 입금 (배치 M2) — 자유 토큰 증가. 좌수는 불변.</summary>
  131. public void AddCash(decimal amount)
  132. {
  133. if (amount < 0)
  134. {
  135. throw new ArgumentOutOfRangeException(nameof(amount));
  136. }
  137. Token += amount;
  138. }
  139. }