using System; using System.Collections.Generic; using System.Linq; namespace YLErp.Derivatives.Interest; // ───────────────────────────────────────────────────────────────────────────── // 词汇表(本文件只允许出现下列用词,同一概念不得出现第二种叫法) // // 概念 唯一用词 与既有代码的对应 // ─────────────────────────────────────────────────────────────────── // 区间起点/终点 Start / End startDate / endDate // 计息 Accrue CalcDailySimpleInterest / CalcDailyCompoundInterest // 平仓 Unwind unwindPercent(既有字段 closePercent) // 已实现利息 Realized realizedInterest(legacy 字段 consumedInterest) // 待实现收益 Unrealized 预付金模式下的待实现收益余额 // 计息基数 principal principal / dynomicPrincipal // 年化天数 annualDays tradeExtend.ExtendObj.AnnualDays // // 入参一律沿用既有代码的字段名,调用点两边读起来同名,不产生心智翻译成本。 // 出参改用自描述名(Accrued / AccruedToday),因为 "Td" 对新读者是黑话。 // ───────────────────────────────────────────────────────────────────────────── /// /// 计息区间边界(算头 / 算尾)。 /// 用具名值取代两个相邻 bool,物理上杜绝 calcFirst / calcLast 传反这一类历史缺陷。 /// public readonly struct AccrualBoundary { /// 算头:含 startDate。 public bool IncludeStart { get; } /// 算尾:含 endDate。 public bool IncludeEnd { get; } private AccrualBoundary(bool includeStart, bool includeEnd) => (IncludeStart, IncludeEnd) = (includeStart, includeEnd); /// 算头算尾 [start, end]。 public static readonly AccrualBoundary Both = new(true, true); /// 算头不算尾 [start, end)。 public static readonly AccrualBoundary StartOnly = new(true, false); /// 不算头算尾 (start, end]。 public static readonly AccrualBoundary EndOnly = new(false, true); /// 不算头不算尾 (start, end)。 public static readonly AccrualBoundary None = new(false, false); /// 由既有 calcFirst / calcLast 布尔对构造,供旧调用方渐进迁移。 public static AccrualBoundary Of(bool includeStart, bool includeEnd) => new(includeStart, includeEnd); public override string ToString() => $"{(IncludeStart ? "算头" : "不算头")}{(IncludeEnd ? "算尾" : "不算尾")}"; } /// /// 计息结果。Accrued → 记账字段 InterestAmount / InterestProfitSum;AccruedToday → TdInterestAmount。 /// public readonly struct InterestResult { /// 区间累计应计利息。 public decimal Accrued { get; } /// 末日(当日)应计利息。 public decimal AccruedToday { get; } public InterestResult(decimal accrued, decimal accruedToday) => (Accrued, AccruedToday) = (accrued, accruedToday); public static readonly InterestResult Zero = new(0m, 0m); public override string ToString() => $"Accrued={Accrued}, AccruedToday={AccruedToday}"; } /// /// 收益互换(TRS)利息腿计算——纯函数。 /// /// 设计约束: /// 1. 无副作用——不读写 flowEvent、不取利率、不连库、不碰任何共享可变状态; /// 2. 同 input → 同 output,结果仅通过返回值流出; /// 3. 正交轴(算头算尾 / 单利复利 / 平仓 / 待实现收益)各自独立,互不耦合; /// 4. 调用方负责「取利率 + 构造日期区间 + 落库」,本类只算账。 /// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。 /// /// 领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的 /// floateRate 单一入参),不存在 IRS 那种 fixedRate − floatingRate 轧差; /// 权益腿盈亏与平仓费用属三腿汇总层,不在本类职责内。 /// /// 为何不复用 Qdp 的 IDayCount: /// a. 语义——Qdp 的 DaysInPeriod = end − start 是写死的半开区间,只能表达四种算头算尾中的一种; /// b. 精度——Qdp 返回 double 年化系数,本系统 decimal 且日息先 Round 再乘天数, /// Round(P*r/365, 11) * n ≠ P*r*(n/365),与 Excel 对账口径不同; /// c. 依赖方向——Qdp 用自有 Date 类型,引入会让 YLErp.Core 反向依赖定价库。 /// public static class SwapInterest { /// 系统统一价格精度位数。 public const int Precision = 11; /// 年化天数常量(合约字段存的是 int,故不用 enum)。 public const int Act365 = 365; public const int Act360 = 360; /// 应计天数。边界规则由日期区间表达,计息函数内不再出现 flag 分支。 public static int AccrualDays(DateTime startDate, DateTime endDate, AccrualBoundary boundary) { var s = boundary.IncludeStart ? startDate : startDate.AddDays(1); var e = boundary.IncludeEnd ? endDate : endDate.AddDays(-1); var days = (int)(e - s).TotalDays + 1; // 含两端 return days < 0 ? 0 : days; } /// 单利:计息基数固定,每日利息相同,无逐日循环。 public static InterestResult AccrueSimple( decimal principal, decimal rate, DateTime startDate, DateTime endDate, AccrualBoundary boundary, int annualDays, int precision = Precision) { var days = AccrualDays(startDate, endDate, boundary); var daily = Round(principal * rate / annualDays, precision); return new InterestResult(Round(daily * days, precision), daily); } /// /// 复利:按重置日切段,段间把累计利息并入计息基数。 /// 段内复用 AccrueSimple(仍无逐日循环);重置日是唯一并本金的地方。 /// public static InterestResult AccrueCompound( decimal principal, decimal rate, DateTime startDate, DateTime endDate, AccrualBoundary boundary, IReadOnlyList resetDates, int annualDays, int precision = Precision) { var basis = principal; decimal accrued = 0m, accruedToday = 0m; var segEnds = (resetDates ?? Array.Empty()) .Where(d => d > startDate && d < endDate) .OrderBy(d => d) .Append(endDate) .ToArray(); var segStart = startDate; var segIncludeStart = boundary.IncludeStart; foreach (var segEnd in segEnds) { var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd); var seg = AccrueSimple(basis, rate, segStart, segEnd, segBoundary, annualDays, precision); accrued += seg.Accrued; accruedToday = seg.AccruedToday; basis += seg.Accrued; // 仅在重置日并本金 segStart = segEnd; segIncludeStart = false; // 后续段不算头 } return new InterestResult(accrued, accruedToday); } /// /// 平仓(Unwind)缩放——全仓唯一缩放点,物理上杜绝 unwindPercent 被重复相乘。 /// 全平即 unwindPercent = 1,不另设方法。 /// /// 已实现 / 未实现边界:传入的 是平仓前仍「未实现(unrealized)」的 /// 累计应计利息;本方法按比例缩放后返回「平仓后剩余未实现」部分,并扣除历史累计「已实现(realized)」 /// 的 。被平仓比例 unwindPercent 对应的那一份 accrued, /// 即在此刻「实现(realized)」,由调用方记入 realizedInterest。 /// /// 平仓前累计应计利息(未实现)。 /// /// 平仓比例(0~1,实为 ratio 非百分数)。 /// 对应既有字段 closePercent;分母口径必须与传入 所依据的持仓数量一致—— /// 是「本次计算依据的持仓」而非「初始建仓」,历史缺陷正来自这个歧义。 /// /// 已实现利息累计(legacy 字段 consumedInterest):历史各次 unwind 已确认、应从剩余未实现中扣除的部分。 public static InterestResult ApplyUnwind( InterestResult accrued, decimal unwindPercent, decimal realizedInterest = 0m, int precision = Precision) { var remaining = 1m - unwindPercent; return new InterestResult( Round(accrued.Accrued * remaining - realizedInterest, precision), Round(accrued.AccruedToday * remaining, precision)); } /// 待实现收益余额滚动(预付金 / 授信模式)。 /// 上期待实现收益余额。 /// 本期新增。 /// 本期 unwind 应扣减(即本期实现的份额)。 public static decimal AccrueUnrealized( decimal openingUnrealized, decimal todayIncome, decimal unwindDeduction, int precision = Precision) => Round(openingUnrealized + todayIncome - unwindDeduction, precision); private static decimal Round(decimal value, int precision) => Math.Round(value, precision, MidpointRounding.AwayFromZero); }