From e2431e9d44c33607cf650d774b82afe67f96e8d4 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 14 Aug 2026 15:31:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(accrual):=20=E8=AE=A1=E6=81=AF?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=95=B4=E4=BD=93=E8=BF=81=E5=85=A5=20DAL?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=96=B0=E5=A2=9E=20Accrual/InterestMath?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=20Core=20=E6=9C=AA=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=E5=AD=A4=E5=84=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搬迁(算法体逐字未动,仅换命名空间与归属): - SwapInterest.Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult → YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs - AccrualTrace → Accrual/AccrualTrace.cs(被迫同迁:其 MarkStart 引用 AccrualBoundary, Core 不能反向依赖 DAL) - 引用切换:Simple/CompoundInterestAccrual、AccrualPolicy、SwapCalcTrace、SwapDealService (保留 using YLErp.Derivatives.Interest——IIndexFixer/IndexFixerBase 留 Core) 删除(零生产引用,孤儿清零): - Core:SwapInterest.cs 算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/ AccrueUnrealized/ToInterestRate,未接线且与 DAL 生产实现舍入/rollover 口径已分叉)、 AccrualContext.cs、InterestRate.cs - DAL:AccrualState.cs(零引用死类) - 测试:SwapInterest_CompoundInArrears_RolloverTimingTests.cs(仅测已删原语) 验证:两解决方案 Rebuild 0 错误;磁盘 SwapInterest. 残留 0;影子/分红/场景 86/86 通过 (含 Accrual 3 影子对账、Margin 影子、divPower 新增 AutoUnwindMultiPartial)。 注:AccrualContext 默认精度 11 与生产 12 的分叉隐患随删除一并消除; 已删原语若将来重建须先补对账测试,勿凭记忆复原(ARCHITECTURE.md 已留警告)。 --- .../YLErp.Core/Interest/AccrualContext.cs | 29 -- Framework/YLErp.Core/Interest/InterestRate.cs | 71 ----- Framework/YLErp.Core/Interest/SwapInterest.cs | 291 ------------------ .../Accrual/CompoundEodShadowTest.cs | 2 - .../Accrual/CompoundPeriodShadowTest.cs | 1 - ...t_CompoundInArrears_RolloverTimingTests.cs | 144 --------- .../Margin/MarginInterestShadowTest.cs | 1 - YLErpDAL/Modules/SwapModule/ARCHITECTURE.md | 18 +- .../SwapModule/Accrual/AccrualPolicy.cs | 4 +- .../SwapModule/Accrual/AccrualState.cs | 49 --- .../SwapModule/Accrual}/AccrualTrace.cs | 12 +- .../Accrual/CompoundInterestAccrual.cs | 15 +- .../SwapModule/Accrual/InterestMath.cs | 104 +++++++ .../Accrual/SimpleInterestAccrual.cs | 15 +- YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs | 5 +- .../Modules/SwapModule/SwapDealService.cs | 7 +- corp-action-refactor-proposal.md | 2 +- 17 files changed, 141 insertions(+), 629 deletions(-) delete mode 100644 Framework/YLErp.Core/Interest/AccrualContext.cs delete mode 100644 Framework/YLErp.Core/Interest/InterestRate.cs delete mode 100644 Framework/YLErp.Core/Interest/SwapInterest.cs delete mode 100644 UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs delete mode 100644 YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs rename {Framework/YLErp.Core/Interest => YLErpDAL/Modules/SwapModule/Accrual}/AccrualTrace.cs (94%) create mode 100644 YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs diff --git a/Framework/YLErp.Core/Interest/AccrualContext.cs b/Framework/YLErp.Core/Interest/AccrualContext.cs deleted file mode 100644 index 2a1f6273..00000000 --- a/Framework/YLErp.Core/Interest/AccrualContext.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace YLErp.Core.Interest; - -/// -/// 计息执行上下文:把"与具体金额/利率无关"的横向参数(年化天数、精度、trace 收集器) -/// 打包成一个只读值对象,避免每个计息方法都重复携带这些参数。 -/// -/// 为何 trace 是"成员"而非散落参数:利息纯函数(AccrueSimple / AccrueCompoundInArrears) -/// 的核心职责是算账,trace 只是可观测性的旁路。把 trace 作为上下文的成员传入, -/// 调用点只需传一个 ctx,签名更干净;同时 ctx 是只读值对象,不破坏纯函数 -/// (无共享可变状态 → 线程安全、可重入、可测)。切勿把 trace 设成类的实例/静态字段, -/// 那会让并发的两笔交易共用同一 trace、并使函数带隐藏状态。 -/// -/// 与 AccrualState(跨日滚动本金状态)/ AccrualPolicy(EOD 会计政策)正交: -/// 本上下文只描述"如何算 + 往哪记",不持有任何交易进度。 -/// -public readonly struct AccrualContext -{ - /// 年化天数(365 / 360)。 - public int AnnualDays { get; } - - /// 舍入精度位数。默认 11(生产融资腿/保证金腿均显式传入 FundingLegPrecision=12)。 - public int Precision { get; } - - /// 可选 trace 收集器;为 null 时不记录(纯计算场景直接传 null,与开关无关)。 - public AccrualTrace? Trace { get; } - - public AccrualContext(int annualDays, int precision = 11, AccrualTrace? trace = null) - => (AnnualDays, Precision, Trace) = (annualDays, precision, trace); -} diff --git a/Framework/YLErp.Core/Interest/InterestRate.cs b/Framework/YLErp.Core/Interest/InterestRate.cs deleted file mode 100644 index b5a6def1..00000000 --- a/Framework/YLErp.Core/Interest/InterestRate.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace YLErp.Core.Interest; - -/// -/// 利率 + 计息方式(单利 / 复利 / 连续复利)。 -/// -/// 通用金融原语,与互换、衍生品、任何具体业务均无耦合——谁需要算利息都能用。 -/// 利息计算不是互换特有的,所以它不住在 SwapModule,也不带任何 swap 词汇。 -/// -/// 用法(年化时间 t,如 30天/365): -/// -/// 计息因子 = ;含息额 = 本金 × 因子; -/// 利息 = 本金 × (因子 − 1) = -/// -/// -/// 与 QuantLib 模型一致:单利 / 复利 / 连续复利只是 的一个分支, -/// 不是三套独立方法。TRS 的"重置日并本金"属于离散复利,用 -/// 按段计息、段末把利息滚入本金即可(见 SwapInterest.AccrueCompoundInArrears),无需 Pow/Exp,decimal 精度无损。 -/// -/// 互换特有的会计态(每日先舍入再乘天数、平仓缩放、跨日滚动本金)不属于本原语, -/// 请在各自的 accrual 层处理。 -/// -public enum Compounding -{ - /// 单利:因子 = 1 + r·t。 - Simple, - /// 复利(理想化闭式):因子 = (1 + r/f)^(f·t),f 为年复利频次。 - Compounded, - /// 连续复利:因子 = e^(r·t)。 - Continuous -} - -/// -/// 不可变利率值对象。构造即完整,无副作用。 -/// -public readonly struct InterestRate -{ - /// 年化利率 r。 - public decimal Rate { get; } - - /// 计息方式。 - public Compounding Compounding { get; } - - /// 年复利频次(仅 使用,其余忽略,默认 1)。 - public int Frequency { get; } - - public InterestRate(decimal rate, Compounding compounding, int frequency = 1) - => (Rate, Compounding, Frequency) = (rate, compounding, frequency); - - /// - /// 计息因子(输入年化时间 t)。 - /// - /// :decimal 精确运算。 - /// / :闭式(double 计算后回 decimal), - /// 满足通用定价;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。 - /// - /// - public decimal CompoundFactor(decimal t) - => Compounding switch - { - Compounding.Simple => 1m + Rate * t, - Compounding.Compounded => (decimal)Math.Pow((double)(1m + Rate / Frequency), (double)(Frequency * t)), - Compounding.Continuous => (decimal)Math.Exp((double)(Rate * t)), - _ => throw new ArgumentOutOfRangeException(nameof(Compounding)) - }; - - /// 利息 = 本金 × (因子 − 1)。 - public decimal Interest(decimal principal, decimal t) - => principal * (CompoundFactor(t) - 1m); -} diff --git a/Framework/YLErp.Core/Interest/SwapInterest.cs b/Framework/YLErp.Core/Interest/SwapInterest.cs deleted file mode 100644 index ddec05bf..00000000 --- a/Framework/YLErp.Core/Interest/SwapInterest.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using YLErp.Core.Interest; - -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)利息腿计算——纯函数。 -/// -/// 层级关系:计息数学(单利/复利/连续复利)是通用金融原语,已抽到 -/// YLErp.Core.Interest,与互换无关,谁都能用)。 -/// 本类只负责 TRS 特有的会计态:每日先舍入再乘天数的对账口径、平仓缩放、 -/// 跨日滚动本金、预付金/授信模式——这些不是"利率数学",不应塞进通用原语。 -/// -/// 设计约束: -/// 1. 无副作用——不读写 flowEvent、不取利率、不连库、不碰任何共享可变状态; -/// 2. 同 input → 同 output,结果仅通过返回值流出; -/// 3. 正交轴(算头算尾 / 单利复利 / 平仓 / 待实现收益)各自独立,互不耦合; -/// 4. 调用方负责「取利率 + 构造日期区间 + 落库」,本类只算账。 -/// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。 -/// -/// 领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的 -/// floateRate 单一入参),不存在 IRS 那种 fixedRate − floatingRate 轧差; -/// 权益腿盈亏与平仓费用属三腿汇总层,不在本类职责内。 -/// -/// TRS 的"复利"是离散重置日复利:按重置日切段,每段用 -/// 计息、段末把利息滚入本金——本质就是单利按段叠加,decimal 精度无损,无需 Pow/Exp -/// (见 )。所以本类不另立复利方法,计息只有一种,区别在于"是否滚动本金"。 -/// -/// 为何不复用 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 -{ - /// 默认舍入精度位数(历史值;生产融资腿与保证金腿均用 FundingLegPrecision=12)。 - public const int Precision = 11; - - /// 资金腿与保证金腿的生产计息精度(落库/对账均以 12 位为准)。 - /// 提升至公共常量,消除 SwapDealService 与 SimpleInterestAccrual 的重复定义。 - public const int FundingLegPrecision = 12; - - /// 年化天数常量(合约字段存的是 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; - } - - /// 把 TRS 年化利率收敛为通用利率原语。 - /// TRS 计息按段均为单利——离散重置日复利靠"段末把利息滚入本金"实现,不引入 Compounded 闭式。 - public static InterestRate ToInterestRate(decimal annualRate) - => new(annualRate, Compounding.Simple); - - /// 单利:计息基数固定,每日利息相同,无逐日循环。 - public static InterestResult AccrueSimple( - AccrualContext ctx, - decimal principal, - decimal rate, - DateTime startDate, - DateTime endDate, - AccrualBoundary boundary) - { - var days = AccrualDays(startDate, endDate, boundary); - var daily = Round(principal * rate / ctx.AnnualDays, ctx.Precision); - return new InterestResult(Round(daily * days, ctx.Precision), daily); - } - - /// - /// 离散重置日复利(compounded-in-arrears):按重置日切段,段间把累计利息并入计息基数(滚动本金)。 - /// 每段计息即 得到的 (无逐日循环); - /// 重置日是唯一并本金的地方。复利与单利只有"是否滚动本金"这一个区别。 - /// - /// 此模型即 OIS / SOFR / FR007 的 compounded-in-arrears:每个子区间取一次定盘 rᵢ、增长因子 - /// 1 + rᵢ·yfᵢ,段末把 accrued 折进下一期本金——比闭式 - /// 更贴合 FR007 约定且 decimal 无损。注意:它不是 InterestRate 的 Compounded 闭式分支(TRS 下该分支为死路径)。 - /// - /// 每段可有独立利率(FR007 浮动逐段不同),由适配器按段取定盘后封装为 - /// 传入——取价永远在编排层,原语只吃一个数(与 QuantLib/Strata 同范)。 - /// 必须含一条 ResetDate ≤ startDate 的起始利率。 - /// - /// trace:经 发射 Start / ResetBefore·ResetAfter(利率切换时) / - /// Rollover(段末并本金) / End,完整记录"重置日前后、利率切换、本金增加前后"。纯函数保持无日志依赖。 - /// - /// 重置日 → 该段生效利率(段起点 = 重置日)。 - public static InterestResult AccrueCompoundInArrears( - AccrualContext ctx, - decimal principal, - IReadOnlyList<(DateTime ResetDate, decimal Rate)> resetSchedule, - DateTime startDate, - DateTime endDate, - AccrualBoundary boundary) - { - var trace = ctx.Trace; - trace?.MarkStart(startDate, endDate, boundary, ctx.AnnualDays, annualized: false); - - var basis = principal; - decimal accrued = 0m, accruedToday = 0m; - - var segEnds = (resetSchedule ?? Array.Empty<(DateTime, decimal)>()) - .Select(s => s.ResetDate) - .Where(d => d > startDate && d < endDate) - .OrderBy(d => d) - .Append(endDate) - .ToArray(); - - // 段起点生效利率:取"不晚于该段起点"的最近一次重置利率。 - decimal RateAt(DateTime segStart) - => (resetSchedule ?? Array.Empty<(DateTime, decimal)>()) - .Where(s => s.ResetDate <= segStart) - .OrderByDescending(s => s.ResetDate) - .Select(s => s.Rate) - .FirstOrDefault(); - - var segStart = startDate; - var segIncludeStart = boundary.IncludeStart; - var prevRate = RateAt(startDate); - - foreach (var segEnd in segEnds) - { - var segRate = RateAt(segStart); - var rateSwitched = segStart != startDate && segRate != prevRate; - if (rateSwitched) trace?.ResetBefore(segStart, prevRate, basis); - - var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd); - var seg = AccrueSimple(ctx, basis, segRate, segStart, segEnd, segBoundary); - - accrued += seg.Accrued; - accruedToday = seg.AccruedToday; - var newBasis = basis + seg.Accrued; // 仅在重置日并本金 - // 重置日本身不动本金:RESET↑ 的本金应是"重置边界基数"(basis),与 RESET↓ 一致; - // 段末并本金后的 newBasis 由下方的 ROLLOVER 单独表达,避免重复/误导。 - if (rateSwitched) trace?.ResetAfter(segStart, segRate, basis); - - trace?.Rollover(segEnd, seg.Accrued, newBasis); - basis = newBasis; - prevRate = segRate; - segStart = segEnd; - segIncludeStart = false; // 后续段不算头 - } - - var result = new InterestResult(accrued, accruedToday); - trace?.MarkEnd(result.Accrued, result.AccruedToday); - return result; - } - - /// - /// 固定利率复利便捷重载(每段同一 rate),向后兼容旧调用方。 - /// 内部把 resetDates 展平为"每段同率"的 schedule 后委托主方法。 - /// - public static InterestResult AccrueCompoundInArrears( - AccrualContext ctx, - decimal principal, - decimal rate, - DateTime startDate, - DateTime endDate, - AccrualBoundary boundary, - IReadOnlyList? resetDates = null) - { - var schedule = new List<(DateTime, decimal)> { (startDate, rate) }; - if (resetDates != null) - foreach (var d in resetDates) - if (d > startDate && d < endDate) - schedule.Add((d, rate)); - return AccrueCompoundInArrears(ctx, principal, schedule, startDate, endDate, boundary); - } - - /// - /// 平仓(Unwind)缩放——全仓唯一缩放点,物理上杜绝 unwindPercent 被重复相乘。 - /// 全平即 unwindPercent = 1,不另设方法。 - /// - /// 已实现 / 未实现边界:传入的 是平仓前仍「未实现(unrealized)」的 - /// 累计应计利息;本方法按比例缩放后返回「平仓后剩余未实现」部分,并扣除历史累计「已实现(realized)」 - /// 的 。被平仓比例 unwindPercent 对应的那一份 accrued, - /// 即在此刻「实现(realized)」,由调用方记入 realizedInterest。 - /// - /// 平仓前累计应计利息(未实现)。 - /// - /// 平仓比例(0~1,实为 ratio 非百分数)。 - /// 对应既有字段 closePercent;分母口径必须与传入 所依据的持仓数量一致—— - /// 是「本次计算依据的持仓」而非「初始建仓」,历史缺陷正来自这个歧义。 - /// - /// 已实现利息累计(legacy 字段 consumedInterest):历史各次 unwind 已确认、应从剩余未实现中扣除的部分。 - /// 舍入精度。⚠️ 默认 11(Precision),资金腿务必显式传 =12。 - 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); - - /// 统一舍入:MidpointRounding.AwayFromZero。所有计息路径收口到此处,避免散落的 Math.Round 不一致。 - public static decimal Round(decimal value, int precision) - => Math.Round(value, precision, MidpointRounding.AwayFromZero); -} diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs index 625d2254..6a1d8984 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs @@ -7,8 +7,6 @@ using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Accrual; -using YLErp.Derivatives.Interest; -using YLErp.Core.Interest; namespace UnitTestProject.Modules.SwapModule.Accrual { diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs index 305fe802..2f6aed6e 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using YLErp; -using YLErp.Derivatives.Interest; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Accrual; diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs b/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs deleted file mode 100644 index 816be349..00000000 --- a/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Text.RegularExpressions; -using YLErp.Core.Interest; -using YLErp.Derivatives.Interest; - -namespace UnitTestProject.Modules.SwapModule.Accrual -{ - /// - /// 聚焦测试:AccrueCompoundInArrears 的「本金滚存时机」必须符合确认书规定。 - /// 核心不变量:本金只允许在重置日/段末滚入利息,非重置日不得资本化。 - /// - /// 与原草稿的关键区别:本版直接通过 AccrualTrace 断言不变量。 - /// 真实实现在每次段末会发出 ROLLOVER 事件并记录 newBasis(见 SwapInterest.cs:215 / - /// AccrualTrace.Rollover),因此「非重置日是否发生资本化」是可程序化验证的, - /// 无需仅靠总利息回归来保护(原草稿的自我怀疑"无法断言计息基数"已不成立)。 - /// - [TestClass] - public class SwapInterest_CompoundInArrears_RolloverTimingTests - { - private const int FundingLegPrecision = 12; - private const int AnnualDays = 365; - - /// - /// 场景:14天窗口,第8天(01-08)重置一次,利率恒定 3.65%(日利率 0.01%)。 - /// 验证: - /// (1) 总利息 = 1400.49(第1期700 + 第2期700.49); - /// (2) ROLLOVER 仅发生在重置日(01-08)与窗口终点(01-15),非重置日(如01-03)绝不滚存; - /// (3) 重置日 ROLLOVER 的 newBasis = 原始本金 + 前7天利息 = 1,000,700, - /// 证明第1段计息基数恒为原始本金、段内未提前资本化。 - /// - [TestMethod] - public void InterestPrincipal_ShouldRollOnlyOnResetDays_NotOnNonResetDays() - { - var startDate = new DateTime(2026, 1, 1); - var endDate = new DateTime(2026, 1, 15); - - var principal = 1_000_000m; - var rate = 0.0365m; - var resetDates = new List { new DateTime(2026, 1, 8) }; - var trace = new AccrualTrace(); - var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace); - - var result = SwapInterest.AccrueCompoundInArrears( - ctx, - principal, - rate, - startDate, - endDate, - AccrualBoundary.Both, - resetDates); - - Assert.AreEqual(1400.49m, Math.Round(result.Accrued, 2)); - - var rolloverDates = trace.Entries - .Where(e => e.Step == AccrualTraceEvent.Rollover) - .Select(e => e.Date) - .ToList(); - - var allowed = resetDates.Concat(new[] { endDate }).OrderBy(d => d).ToList(); - CollectionAssert.AreEqual(allowed, rolloverDates.OrderBy(d => d).ToList()); - - Assert.IsFalse(rolloverDates.Contains(new DateTime(2026, 1, 3)), - "非重置日发生了本金滚存,违反确认书规定"); - - var resetRollover = trace.Entries - .First(e => e.Step == AccrualTraceEvent.Rollover && e.Date == new DateTime(2026, 1, 8)); - var newBasis = ParseNewBasis(resetRollover.Line); - Assert.AreEqual(principal + 700m, newBasis, - "重置日滚入的本金应为原始本金 + 前段利息,证明段内未提前资本化"); - } - - /// - /// 极端场景:startDate = endDate(1天),无重置日。 - /// 期望利息 = 本金 × 日利率 = 1,000,000 × 0.0365/365 = 100。 - /// 且唯一 ROLLOVER 必须落在窗口终点(=startDate),无任何内部重置滚存。 - /// - [TestMethod] - public void SingleDay_ShouldNotRollInterest_NoResetDay() - { - var date = new DateTime(2026, 1, 1); - var principal = 1_000_000m; - var rate = 0.0365m; - var trace = new AccrualTrace(); - var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace); - - var result = SwapInterest.AccrueCompoundInArrears( - ctx, - principal, - rate, - date, - date, - AccrualBoundary.Both); - - Assert.AreEqual(100m, Math.Round(result.Accrued, 2)); - - var rolloverDates = trace.Entries - .Where(e => e.Step == AccrualTraceEvent.Rollover) - .Select(e => e.Date) - .ToList(); - CollectionAssert.AreEqual(new[] { date }, rolloverDates.ToArray()); - } - - /// - /// 段内无重置日:验证整段等同于单利,且不发生任何内部滚存。 - /// 6天窗口(01-01..01-06)在7天重置周期内,Both 边界含两端 = 6 个计息日, - /// 期望利息 = 本金 × 日利率 × 6 = 600。 - /// - [TestMethod] - public void WithinPeriod_NoRollover_ShouldMatchSimpleInterest() - { - var startDate = new DateTime(2026, 1, 1); - var endDate = new DateTime(2026, 1, 6); - var principal = 1_000_000m; - var rate = 0.0365m; - var trace = new AccrualTrace(); - var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace); - - var result = SwapInterest.AccrueCompoundInArrears( - ctx, - principal, - rate, - startDate, - endDate, - AccrualBoundary.Both); - - // 计息天数必须用边界感知的 AccrualDays,不能拿 (end-start).Days(会少算1天) - var days = SwapInterest.AccrualDays(startDate, endDate, AccrualBoundary.Both); // = 6 - var expected = Math.Round(principal * rate * days / AnnualDays, FundingLegPrecision, MidpointRounding.AwayFromZero); - Assert.AreEqual(expected, Math.Round(result.Accrued, 10)); - - var rolloverDates = trace.Entries - .Where(e => e.Step == AccrualTraceEvent.Rollover) - .Select(e => e.Date) - .ToList(); - CollectionAssert.AreEqual(new[] { endDate }, rolloverDates.ToArray()); - } - - private static decimal ParseNewBasis(string line) - { - var m = Regex.Match(line, @"newBasis=([0-9.]+)"); - Assert.IsTrue(m.Success, $"ROLLOVER 行缺少 newBasis:{line}"); - return decimal.Parse(m.Groups[1].Value); - } - } -} diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs index 34740fc2..2ff0f5ad 100644 --- a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs @@ -8,7 +8,6 @@ using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Accrual; using YLErp.Modules.SwapModule.Margin; -using YLErp.Derivatives.Interest; namespace UnitTestProject.Modules.SwapModule.Margin { diff --git a/YLErpDAL/Modules/SwapModule/ARCHITECTURE.md b/YLErpDAL/Modules/SwapModule/ARCHITECTURE.md index 1ddf3913..669e64ff 100644 --- a/YLErpDAL/Modules/SwapModule/ARCHITECTURE.md +++ b/YLErpDAL/Modules/SwapModule/ARCHITECTURE.md @@ -62,6 +62,14 @@ SwapModule/ │ ├── DirectionRatio 方向因子(LongShort + ReceivePay) │ └── PositionValueCalc 持仓价值汇总(利息端 + 浮动端) │ +├── Accrual/ 计息(生产实现,自洽域) +│ ├── InterestMath 共用数学:Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult +│ ├── SimpleInterestAccrual 单利纯函数(AccrueEod 单日 + AccruePeriod 多日) +│ ├── CompoundInterestAccrual 复利纯函数(EodBasis/AccrueEod/AccruePeriod) +│ ├── AccrualPolicy 计息政策(算头算尾/单复利/重置周期/年化) +│ ├── AccrualTrace 计息 trace 收集器(SwapCalcTrace.Write 常驻落盘) +│ └── FundingLegRate all-in 利率值对象 +│ ├── SwapDealService.cs 盘中平仓/互换主逻辑 ├── SwapEodPositionService.cs EOD 日终归档主逻辑 ├── SwapDealIndexFixer.cs SwapDealService 专用取价器(委托 TryGetFloatRate) @@ -72,12 +80,16 @@ SwapModule/ ``` Interest/ -├── SwapInterest.cs 纯函数库(AccrueSimple/AccrueCompound/ApplyUnwind) ├── IIndexFixer.cs 取价接口 -├── IndexFixerBase.cs 取价日计算工具 -└── Fr007IndexFixer.cs FR007 取价生产实现(调 EodPriceQueryService) +└── IndexFixerBase.cs 取价日计算工具 ``` +> 注:① `Fr007IndexFixer.cs`(FR007 取价生产实现)在 SwapModule 下,不在本目录。 +> ② 2026-08 计息类型(InterestMath/AccrualBoundary/InterestResult/AccrualTrace)已整体迁至 SwapModule/Accrual/, +> Core 不再持有计息实现。原 Core 层 SwapInterest 的算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/ +> AccrueUnrealized/ToInterestRate)与 AccrualContext/InterestRate 从未接线(生产走 Accrual/ 目录),作为孤儿死代码删除—— +> 其舍入/rollover 口径与生产实现已分叉,若将来重建须先补对账测试,勿凭记忆复原。 + ## InterestModeEnum(显式赋值,DB 契约) ``` diff --git a/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs b/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs index 9f7e4ec4..324a373b 100644 --- a/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs @@ -1,5 +1,3 @@ -using YLErp.Derivatives.Interest; - namespace YLErp.Modules.SwapModule.Accrual; /// @@ -11,7 +9,7 @@ namespace YLErp.Modules.SwapModule.Accrual; /// public sealed class AccrualPolicy { - /// 算头算尾约定(复用 SwapInterest 已有的 AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。 + /// 算头算尾约定(AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。 public AccrualBoundary Convention { get; } /// 是否复利(利滚利)。来自 DB 的 InterestTypeEnum;单利=false,复利=true。 diff --git a/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs b/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs deleted file mode 100644 index 3ba2c6fb..00000000 --- a/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs +++ /dev/null @@ -1,49 +0,0 @@ -using YLErp.DBModels; - -namespace YLErp.Modules.SwapModule.Accrual; - -/// -/// 融资腿逐日计息的跨日状态(不可变值对象)。 -/// 这是"待实现利息"在日间滚动的快照,区别于已落库的 swap_flow_event。 -/// -/// 旧字段 → 领域命名映射(DB 列不可改,仅在边界处适配;本类内部一律用下列自描述名): -/// -/// TdInterestPrincipal逐日滚动的计息本金 → -/// InterestIncomeSum累计待实现利息 → -/// consumedInterest历史已实现利息(legacy) → -/// ValueDate快照截至日 → (EOD 续接起算日,Bug C / 5-11 跳过需据此判断从哪天接续)。 -/// -/// -public readonly struct AccrualState -{ - /// 用于计算当日利息的计息本金。单利=名义本金基数;复利=本金+累计利息。 - public decimal AccrualPrincipal { get; } - - /// 累计待实现(未平仓)利息。 - public decimal UnrealizedInterest { get; } - - /// 历史各次平仓已确认的已实现利息,从剩余待实现中扣除。 - public decimal RealizedInterest { get; } - - /// 快照截至日(来自 eod_swap_position.ValueDate)。编排层据此判断计息区间起点,避免 5-11 等"跳过日"误重算。 - public DateTime ValueDate { get; } - - public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest, DateTime valueDate) - => (AccrualPrincipal, UnrealizedInterest, RealizedInterest, ValueDate) = (accrualPrincipal, unrealizedInterest, realizedInterest, valueDate); - - /// 向后兼容:未携带快照日期时(如纯内存构造)用默认日。 - public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest) - : this(accrualPrincipal, unrealizedInterest, realizedInterest, default) { } - - /// 空状态(新开仓首个计息日之前)。 - public static readonly AccrualState Zero = new(0m, 0m, 0m); - - /// - /// 从上一日日终归档 适配(边界适配:DB 列名 → 领域名)。 - /// 仅映射计息状态;名义本金基数 / 平仓比例 / 已实现利息等由调用方另行传入。 - /// - public static AccrualState FromPreviousEod(eod_swap_position previousEod) - => previousEod == null || previousEod.id == 0 - ? Zero - : new AccrualState(previousEod.TdInterestPrincipal, previousEod.InterestIncomeSum, 0m, previousEod.ValueDate); -} diff --git a/Framework/YLErp.Core/Interest/AccrualTrace.cs b/YLErpDAL/Modules/SwapModule/Accrual/AccrualTrace.cs similarity index 94% rename from Framework/YLErp.Core/Interest/AccrualTrace.cs rename to YLErpDAL/Modules/SwapModule/Accrual/AccrualTrace.cs index 108b2692..271a0bc4 100644 --- a/Framework/YLErp.Core/Interest/AccrualTrace.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/AccrualTrace.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using YLErp.Derivatives.Interest; - -namespace YLErp.Core.Interest; +namespace YLErp.Modules.SwapModule.Accrual; /// -/// 计息过程追踪收集器(值对象,非日志)。 +/// 计息过程追踪收集器(值对象,非日志)。2026-08 自 Core 层(YLErp.Core.Interest)迁入 DAL, +/// 与 Simple/CompoundInterestAccrual、AccrualBoundary 同处一域,Core 不再持有计息类型。 /// -/// 为什么是收集器而不是日志调用:计息数学(SwapInterest / FundingLegAccrual)必须保持纯函数、 +/// 为什么是收集器而不是日志调用:计息数学(Simple/CompoundInterestAccrual)必须保持纯函数、 /// 可单测、不依赖 NLog;但按工程铁律,关键路径日志须无条件常驻落盘(出问题时事后翻日志定位,不能依赖开关)。 /// 折中:纯函数把"发生了什么"记录为结构化条目写入本收集器,由适配器(IO 边界)统一经 /// SwapCalcTrace.Write 常驻落盘。落盘职责归一处,计息代码零日志依赖、保持干净。 diff --git a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs index 9418aa46..eddbb07a 100644 --- a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs @@ -1,6 +1,3 @@ -using YLErp.Core.Interest; -using YLErp.Derivatives.Interest; - namespace YLErp.Modules.SwapModule.Accrual; /// @@ -9,7 +6,7 @@ namespace YLErp.Modules.SwapModule.Accrual; /// public static class CompoundInterestAccrual { - private const int Precision = SwapInterest.FundingLegPrecision; + private const int Precision = InterestMath.FundingLegPrecision; /// 复利日终计息基数(单一真相源,纯函数与调用方共用): /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。 @@ -52,8 +49,8 @@ public static class CompoundInterestAccrual var totalAccrued = priorAccrued * unwindFraction + dayInterest; var result = new InterestResult( - SwapInterest.Round(totalAccrued, Precision), - SwapInterest.Round(tdInterest, Precision)); + InterestMath.Round(totalAccrued, Precision), + InterestMath.Round(tdInterest, Precision)); trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); trace?.MarkEnd(result.Accrued, result.AccruedToday); @@ -107,7 +104,7 @@ public static class CompoundInterestAccrual var segIncludeStart = (si == 0) ? boundary.IncludeStart : true; var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false; - var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd, + var days = InterestMath.AccrualDays(segmentRates[si].StartDate, segEnd, AccrualBoundary.Of(segIncludeStart, segIncludeEnd)); if (days <= 0) continue; @@ -124,8 +121,8 @@ public static class CompoundInterestAccrual accrued -= realizedInterest * unwindFraction; var result = new InterestResult( - SwapInterest.Round(accrued, Precision), - SwapInterest.Round(accrued, Precision)); + InterestMath.Round(accrued, Precision), + InterestMath.Round(accrued, Precision)); trace?.MarkEnd(result.Accrued, result.AccruedToday); return result; } diff --git a/YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs b/YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs new file mode 100644 index 00000000..54b5de29 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs @@ -0,0 +1,104 @@ +namespace YLErp.Modules.SwapModule.Accrual; + +// ───────────────────────────────────────────────────────────────────────────── +// 词汇表(本文件只允许出现下列用词,同一概念不得出现第二种叫法) +// +// 概念 唯一用词 与既有代码的对应 +// ─────────────────────────────────────────────────────────────────── +// 区间起点/终点 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}"; +} + +/// +/// 利息腿共用数学工具:舍入、应计天数、精度常量。 +/// +/// 沿革:2026-08 自 Core 层 SwapInterest 迁入 DAL(生产消费面整体搬家)。 +/// 原 SwapInterest 的算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/AccrueUnrealized) +/// 与 AccrualContext/InterestRate 始终未接线(生产计息走本目录 Simple/CompoundInterestAccrual, +/// 两者舍入与 rollover 口径已分叉),作为孤儿死代码删除——接线前须先补对账,勿凭记忆重建。 +/// +/// 为何不复用 Qdp 的 IDayCount: +/// a. 语义——Qdp 的 DaysInPeriod = end − start 是写死的半开区间,只能表达四种算头算尾中的一种; +/// b. 精度——Qdp 返回 double 年化系数,本系统 decimal 对账; +/// c. 依赖方向——Qdp 用自有 Date 类型,引入会让本模块反向依赖定价库。 +/// +public static class InterestMath +{ + /// 资金腿与保证金腿的生产计息精度(落库/对账均以 12 位为准)。 + /// 提升至公共常量,消除 SwapDealService 与 SimpleInterestAccrual 的重复定义。 + public const int FundingLegPrecision = 12; + + /// 应计天数。边界规则由日期区间表达,计息函数内不再出现 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; + } + + /// 统一舍入:MidpointRounding.AwayFromZero。所有计息路径收口到此处,避免散落的 Math.Round 不一致。 + public static decimal Round(decimal value, int precision) + => Math.Round(value, precision, MidpointRounding.AwayFromZero); +} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs index fb1378ab..ac184645 100644 --- a/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs @@ -1,6 +1,3 @@ -using YLErp.Core.Interest; -using YLErp.Derivatives.Interest; - namespace YLErp.Modules.SwapModule.Accrual; /// @@ -9,7 +6,7 @@ namespace YLErp.Modules.SwapModule.Accrual; /// public static class SimpleInterestAccrual { - private const int Precision = SwapInterest.FundingLegPrecision; + private const int Precision = InterestMath.FundingLegPrecision; /// /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。 @@ -38,8 +35,8 @@ public static class SimpleInterestAccrual var totalAccrued = priorAccrued + dayInterest; var result = new InterestResult( - SwapInterest.Round(totalAccrued, Precision), - SwapInterest.Round(tdInterest, Precision)); + InterestMath.Round(totalAccrued, Precision), + InterestMath.Round(tdInterest, Precision)); trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); trace?.MarkEnd(result.Accrued, result.AccruedToday); @@ -85,7 +82,7 @@ public static class SimpleInterestAccrual var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true; var isLastSegment = si == segmentRates.Count - 1; var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd); - var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary); + var days = InterestMath.AccrualDays(effectiveStart, segEnd, segBoundary); if (days <= 0) { segStart = segEnd; continue; } var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; @@ -98,8 +95,8 @@ public static class SimpleInterestAccrual } var result = new InterestResult( - SwapInterest.Round(accrued, Precision), - SwapInterest.Round(accruedUnscaled, Precision)); + InterestMath.Round(accrued, Precision), + InterestMath.Round(accruedUnscaled, Precision)); trace?.MarkEnd(result.Accrued, result.AccruedToday); return result; } diff --git a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs index f6e1a1ec..40bc9571 100644 --- a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs +++ b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; using System.Text; -using YLErp.Core.Interest; -using YLErp.Helpers; +using YLErp.Modules.SwapModule.Accrual; namespace YLErp.Modules.SwapModule { diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7990a964..7e69e78b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using YLErp.BLL; using YLErp.BLL.Eod; using YLErp.DBModels.Enums; -using YLErp.Core.Interest; using YLErp.Derivatives.Interest; using YLErp.Helpers; using YLErp.Modules.DataProviderModule; @@ -50,8 +49,8 @@ namespace YLErp.Modules.SwapModule return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve); } - // 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 SwapInterest.FundingLegPrecision,消除重复定义。 - private const int InterestCalculationPrecision = SwapInterest.FundingLegPrecision; + // 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 InterestMath.FundingLegPrecision,消除重复定义。 + private const int InterestCalculationPrecision = InterestMath.FundingLegPrecision; // 客户现金在 SaveSwapDeal 之前创建,手工结算必须先收敛流水并重算汇总金额。 private void NormalizeManualSettlementAmounts(UnwindData unwindData, int eventType, string eventReason) @@ -1456,7 +1455,7 @@ namespace YLErp.Modules.SwapModule SwapCalcTrace.Write(interestTrace); // flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。 - // 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。 + // 复用 CompoundEodBasis 单一真相源(与 CompoundInterestAccrual.AccrueEod 内部同一公式,见其 EodBasis 调用)。 flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis( isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction, preEodPosition.TdInterestPrincipal) * closePercent; diff --git a/corp-action-refactor-proposal.md b/corp-action-refactor-proposal.md index b21d529b..6ec51e4e 100644 --- a/corp-action-refactor-proposal.md +++ b/corp-action-refactor-proposal.md @@ -99,7 +99,7 @@ curretEod.PosiQuantity = qty < 0 ? 0 : Math.Abs(qty); | **数量递推** | `SwapEodPositionService.cs:1897`(qty 递推)、`:1723`/`:1905`(无事件日结转)、`:1631`(首次归档) | 需新增「公司行为数量」第三来源项 | `SwapEodPositionService.cs` | | **`TdChangedQty`** | 定义 `EodSwapPosition.cs:300`(DisplayName "当日公司行为数量");唯一赋值 `SwapEodPositionService.cs:1650`(恒=0) | 挂进 :1897 递推式(与 `TdCloseQty`:1942 对称),否则与 `PosiQuantity` 永久不自洽 | `SwapEodPositionService.cs` | | **成本均价** | `SwapEodPositionService.cs:1926-1936`(加权重算,TRS 无 `CostPrice` 字段,等价字段 `PosiGrossPrice`/`PosiNetPrice`) | 送股无成交金额(分子+0、分母增)→ 走 `:1912 else if` 分支价不摊薄,污染盯市;配股有现金需加 `RationedSharesAmount×Price` | `SwapEodPositionService.cs:1912-1937` | -| **计息基准** | `SwapDealService.cs:893 CalcNotionalByMode`(五种模式分流)、`:1372` `dynomicPrincipal`、`:2384 InterestPrincipalFix` 仅平仓递减 | `posiLong/Short` 经 `PosiNotionalValue` 可自动跟随;**`InterestPrincipalFix` 是独立存量,与数量解耦**,配股缴款需新写入点 | `SwapDealService.cs` | +| **计息基准** | mode 分流已重构为 `FundingLegs/FundingLegStrategyFactory`(原 `SwapDealService CalcNotionalByMode` 已删;`dynomicPrincipal` 亦随重构消失)、`InterestPrincipalFix` 仅平仓递减 | `posiLong/Short` 经 `PosiNotionalValue` 可自动跟随;**`InterestPrincipalFix` 是独立存量,与数量解耦**,配股缴款需新写入点 | `SwapDealService.cs` + `FundingLegs/` | | **盯市盈亏** | `SwapEodPositionService.cs:1657/1730/1815/2019`(4 份同构副本 `PosiMtmPnL`)、`:1713 GetSwapValuationPrice`(取除权后价) | 数量突变日若 `PosiQuantity`/`PosiGrossPrice` 未同步除权 → 虚假巨亏;**4 处副本必须一致改** | `SwapEodPositionService.cs` | | **数据源** | `DividendService.cs:752 GetPositionAmount`(现成 `amount*(1+GiveShareAmount/10)` 送股调整)、`:730 GetRatio`(现成除权价公式) | SwapModule 未复用,需建调用边 | 新增 SwapModule→DividendService 调用 |