diff --git a/Framework/YLErp.Core/Interest/SwapInterest.cs b/Framework/YLErp.Core/Interest/SwapInterest.cs new file mode 100644 index 00000000..f9ca0ad3 --- /dev/null +++ b/Framework/YLErp.Core/Interest/SwapInterest.cs @@ -0,0 +1,213 @@ +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); +}