From 9b29211d3c3f7fa64db8b838273acf1071fa047e Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 13 Aug 2026 10:39:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(margin):=20=E4=BF=9D=E8=AF=81=E9=87=91?= =?UTF-8?q?=E8=AE=A1=E6=81=AF=E5=88=87=E6=8D=A2=E5=88=B0=20CalcMarginInter?= =?UTF-8?q?est=20+=20=E5=88=A0=20orginPv=20=E5=A4=96=E9=83=A8=E7=BB=B4?= =?UTF-8?q?=E5=BA=A6=20hack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GetInterests 保证金分支(5/6)切到 CalcMarginInterest,不再走融资腿通用 CalcEodInterest/CalcUnwindInterest - 删除 InitSwapDealInterest 的保证金 orginPv 维度 hack(保证金已不走该方法,成为死代码) - CalcMarginInterest 修正三处与旧管线的对齐(全量回归发现): · 加 endDate 参数(盘中用 InitInterestDate 的 endDate,否则少算天数) · calcLast 合并 newCalcLast(与 CalcUnwindInterest 一致,算尾) · 盘中保留 accrualBasis 差分(posiPrincipal 对齐状态路径相关,单一本金变量无法覆盖;orginPv 内部按 PreviousBalance 算,消除外部维度 hack) - 更新 Shadow/GoldenReplay 测试调用点(加 endDate) 修正说明:盘中“消除差分”不可行——SPC_006(position 已对齐) 与 PrepaidPrincipalCloseTrace(position 未对齐) 期望相反,差分 accrualBasis 经 orginPv 自适应两种状态。CalcMarginInterest 真实收益收敛为:保证金领域独立 + orginPv 内聚 + 消除浮动/分段/复利死分支,而非消除差分。EOD 路径无差分(用昨日终本金)。 验证:全量 SwapModule 522/522 通过(0 失败);真实库黄金回放 60 条 0 差异。 --- .../Margin/MarginInterestGoldenReplayTest.cs | 2 +- .../Margin/MarginInterestShadowTest.cs | 10 ++--- .../Modules/SwapModule/SwapDealService.cs | 45 +++++++++++-------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs index c9c226d3..22c41d27 100644 --- a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs @@ -125,7 +125,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin swap_flow_event newEvt; try { - newEvt = svc.CalcMarginInterest(td, valueDate, posClone, rate, + newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, posClone, rate, pos.InterestPrincipalFix, pos.InterestPrincipalFix, 1.0m, annualDays, calcFirst, calcLast, preEod, (int)SwapEventTypeEnum.自动互换, add: false, settment: true, swap: false); diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs index 603c49c0..4347a29e 100644 --- a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs @@ -90,7 +90,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd); // 新方法(独立 preEod,相同初始值) - var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m, + var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m, AnnualDays, calcFirst: true, calcLast: true, CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: true, swap: false); @@ -114,7 +114,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin valueDate, td.StartDate.Value, position, Principal, Principal, new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd); - var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m, + var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m, AnnualDays, calcFirst: true, calcLast: true, new eod_swap_position { id = 0 }, 0, add: false, settment: true, swap: false); @@ -143,7 +143,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin calcFirst: true, calcLast: false, ref oldI, ref oldTd); // 新方法:notional = posiPrincipal(无差分、无 orginPv) - var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m, + var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m, AnnualDays, calcFirst: true, calcLast: false, CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false); @@ -171,7 +171,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, closePct, orginPv, calcFirst: true, calcLast: false, ref oldI, ref oldTd); - var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, + var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal * closePct, Principal, closePct, AnnualDays, calcFirst: true, calcLast: false, CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false); @@ -191,7 +191,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin var valueDate = StartDate.AddDays(5); var svc = new StubSvc(); - var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m, + var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m, AnnualDays, calcFirst: true, calcLast: false, CreatePreEod(StartDate.AddDays(4), 820m), 0, add: false, settment: false, swap: true); diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index fec4dfaf..2a2072ae 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -697,7 +697,13 @@ namespace YLErp.Modules.SwapModule decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone); // 根据场景计算利息 - if (settment) + if (MarginModes.Contains(position.InterestMode)) + { + // 保证金腿(5/6):专属计息,notional 直接取保证金余额,无融资腿差分公式与 orginPv 维度 hack + interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate, closePrincipal, posiPrincipal, + newClosePercent, annualDays, calcFirst, calcLast||newCalcLast, preEodPosition, eventType, add, settment, swap)); + } + else if (settment) { // 收盘归档场景,使用 CalcEodInterest interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add)); @@ -921,17 +927,18 @@ namespace YLErp.Modules.SwapModule /// /// 保证金是纯固定利率单利:浮动利率(FR007)/分段利率/复利对其均为死分支(前端无入口、 /// 确认书不含、FundingLegRate.Build 对空 FloatRateUnderlyingCode 恒返回 Fixed)。故本方法直接用 - /// SimpleInterestAccrual 纯函数计息,notional 取保证金余额本身: - /// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源) - /// 盘中 = 今日本金 posiPrincipal(InterestPrincipalFix) - /// 消除融资腿差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 与 orginPv 维度 - /// hack——对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余。保留累计语义(priorAccrued + 增量), - /// 满足下游 SwapEodPositionService 字段契约(InterestAmount=缩放累计、TdInterestAmount=单日参考等)。 + /// SimpleInterestAccrual 纯函数计息,本金取保证金余额: + /// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源,无差分) + /// 盘中 = accrualBasis(preEod.TdInterestPrincipal + posiPrincipal - orginPv) + /// 盘中保留差分是必要的:posiPrincipal 是否经 ResolveInterestLegPositions 对齐到实时剩余是路径相关的 + /// (生产对齐 / 诊断测试用原始腿),单一本金变量无法覆盖两种状态,差分经 orginPv 自适应。orginPv 在 + /// 本方法内部按保证金维度计算(PreviousBalance),消除原 InitSwapDealInterest 的外部维度 hack + /// (融资腿 orginPv=浮动端名义本金)。保留累计语义(priorAccrued + 增量),满足下游字段契约。 /// /// true=收盘归档(EOD),false=盘中平仓/互换。 /// 互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。 public swap_flow_event CalcMarginInterest( - trade td, DateTime valueDate, swap_position position, decimal rate, + trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate, decimal closePrincipal, decimal posiPrincipal, decimal closePercent, int annualDays, bool calcFirst, bool calcLast, eod_swap_position preEod, int eventType, bool add, bool settment, bool swap) @@ -967,7 +974,7 @@ namespace YLErp.Modules.SwapModule FloatRate = 0m, DataState = (int)SwapFlowDateStateEnum.完成, ClientId = td.ClientId, - UnwindDate = valueDate + UnwindDate = settment ? valueDate : endDate }; // 互换事件:利息归零(同 InitSwapDealInterest) @@ -1000,22 +1007,27 @@ namespace YLErp.Modules.SwapModule } else { - // 盘中:notional = 今日本金 posiPrincipal(无差分、无 orginPv) + // 盘中:accrualBasis 自适应"实时剩余本金"——posiPrincipal 已对齐(ResolveInterestLegPositions) + // 时 = posiPrincipal;未对齐的原始腿经 orginPv(=PreviousBalance 昨日终) 修正回昨日终剩余。 + // 单一本金变量无法覆盖两种 position 状态,故保留差分(与 EOD 直接用 preEod.TdInterestPrincipal 不同)。 + // orginPv 在此内部按保证金维度计算,消除原 InitSwapDealInterest 的外部维度 hack。 + var orginPv = MarginCalc.PreviousBalance(preEod, posiPrincipal); + var accrualBasis = preEod.TdInterestPrincipal + posiPrincipal - orginPv; var segmentRates = new List<(DateTime, decimal)> { (position.PosiStartDate, rate) }; var r = SimpleInterestAccrual.AccruePeriod( priorAccrued: preEod.InterestProfitSum * closePercent, - notional: posiPrincipal, + notional: accrualBasis, unwindFraction: closePercent, segmentRates: segmentRates, startDate: position.PosiStartDate, - endDate: valueDate, + endDate: endDate, priorValueDate: preEod.ValueDate, boundary: AccrualBoundary.Of(calcFirst, calcLast), annualDays: annualDays, isAnnualized: position.IsAnnualized); interestAmount = r.Accrued; tdInterestAmount = r.AccruedToday; - interest.InterestPrincipal = posiPrincipal * closePercent; // 同 CalcDailySimpleInterest:1304 + interest.InterestPrincipal = accrualBasis * closePercent; // 同 CalcDailySimpleInterest:1304 } } @@ -1135,11 +1147,8 @@ namespace YLErp.Modules.SwapModule interest.ClientId = td.ClientId; interest.UnwindDate = endDate; - // 保证金腿的 orginPv 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值 - if (MarginModes.Contains(position.InterestMode)) - { - orginPv = MarginCalc.PreviousBalance(preEodPosition, position.InterestPrincipalFix); - } + // 保证金腿已走 CalcMarginInterest(不经过本方法),orginPv 维度重映射不再需要; + // orginPv 此处仅对融资腿生效(差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv)。 if (swap) {