From ec9c493420b942d84bbcb0bdff6d330a694c4a62 Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 17 Aug 2026 16:25:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor(swap-eod):=20=E5=88=A9=E6=81=AF?= =?UTF-8?q?=E8=85=BF=E5=88=86=E6=B4=BE=E5=8F=AF=E8=AF=BB=E6=80=A7=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=E2=80=94=E2=80=94=E8=A7=82=E5=AF=9F=E6=97=A5=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=BB=9F=E4=B8=80+=E5=91=BD=E5=90=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0+=E6=9F=A5=E6=89=BE=E5=8A=A9=E6=89=8B+=E5=88=86?= =?UTF-8?q?=E6=B4=BE=E8=A7=84=E6=A0=BC=E7=BA=AF=E5=87=BD=E6=95=B0=EF=BC=88?= =?UTF-8?q?=E9=9B=B6=E8=A1=8C=E4=B8=BA=E5=8F=98=E6=9B=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - insterval/autoInterval → observationInterval,SwapIntervalList→FindObservationInterval→形参 interval 全链同名 - :512/:525 调用点命名参数 autoSwap: true/false,签名未动 - 抽取 FindObservationInterval 消除两处重复观察日谓词(表达式逐字保留,null 语义不变) - 新增 InterestEodScenario 枚举 + ResolveInterestScenario 影子纯函数 + 8 组合表驱动 InterestEodScenarioDispatchTest(分派结构未接线,生产执行路径不变) - 编译 0 错误;表驱动 8/8 通过 --- .../SwapModule/DealInterestsScenarioTest.cs | 2 +- .../InterestEodScenarioDispatchTest.cs | 33 ++++++++++ .../SwapModule/SwapEodPositionService.cs | 66 +++++++++++++++---- 3 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/InterestEodScenarioDispatchTest.cs diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs index 079a6ba7..f2ab23bc 100644 --- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -447,7 +447,7 @@ namespace YLErp.Modules.SwapModule /// /// [DI_BRANCH_001] 普通日(无互换无平仓无观察日)→ 走 copy 分支 /// --------------------------------------------------------------- - /// flowEvents 为空,insterval=null,hasSwap=false,hasClose=false + /// flowEvents 为空,observationInterval=null,hasSwap=false,hasClose=false /// → 应走 SaveEodInterestPositionCopy(cs:338) /// --------------------------------------------------------------- /// diff --git a/UnitTestProject/Modules/SwapModule/InterestEodScenarioDispatchTest.cs b/UnitTestProject/Modules/SwapModule/InterestEodScenarioDispatchTest.cs new file mode 100644 index 00000000..b0485614 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/InterestEodScenarioDispatchTest.cs @@ -0,0 +1,33 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace YLErp.Modules.SwapModule +{ + /// + /// DealInterests 分派优先级(TEST-MATRIX §6 空洞补盖)。 + /// 纯函数 ResolveInterestScenario 的 8 组合表驱动单测,不连库、无 DB。 + /// 守卫:手工互换压制观察日自动结息;观察日±平仓区分 autoSwap 真/假;纯平仓与普通日分流。 + /// 测试类直接继承 TestableSwapEodPositionService 以访问 protected 的枚举与方法。 + /// + [TestClass] + public class InterestEodScenarioDispatchTest : TestableSwapEodPositionService + { + public InterestEodScenarioDispatchTest() : base(nameof(InterestEodScenarioDispatchTest)) { } + + [DataTestMethod] + [DataRow(false, false, false, InterestEodScenario.RollForward)] // 普通日 + [DataRow(false, false, true, InterestEodScenario.CloseOnly)] // 纯平仓(非观察日) + [DataRow(false, true, false, InterestEodScenario.ManualSwap)] // 手工互换(非观察日) + [DataRow(false, true, true, InterestEodScenario.ManualSwap)] // 手工互换+平仓 → 手工优先 + [DataRow(true, false, false, InterestEodScenario.AutoSettle)] // 观察日, 无平仓 + [DataRow(true, false, true, InterestEodScenario.AutoSettleWithClose)] // 观察日+平仓 (autoSwap=true) + [DataRow(true, true, false, InterestEodScenario.ManualSwap)] // 观察日+手工互换 → 手工优先 + [DataRow(true, true, true, InterestEodScenario.ManualSwap)] // 观察日+手工互换+平仓 → 手工优先 + public void ResolveInterestScenario_CoversAllEightCombinations( + bool hasInterval, bool hasSwap, bool hasClose, InterestEodScenario expected) + { + var actual = ResolveInterestScenario(hasInterval, hasSwap, hasClose); + Assert.AreEqual(expected, actual, + $"hasInterval={hasInterval}, hasSwap={hasSwap}, hasClose={hasClose}"); + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index a5344564..5a3ac28b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -371,16 +371,16 @@ namespace YLErp.Modules.SwapModule var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0; //处理利息腿 DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv); - //获取自动互换的 interval 信息,用于确定结算日期 - IntervalModel autoInterval = null; + //获取自动互换的观察日信息,用于确定结算日期 + IntervalModel observationInterval = null; foreach (var interest in interestList) { - autoInterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1); - if (autoInterval != null) + observationInterval = FindObservationInterval(interest, settleDate); + if (observationInterval != null) break; } // 自动互换(仅利息/预付金,不含分红) - DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval); + DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, observationInterval); // 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关 DealDividends(curEodPosis, td, settleDate, tradeExtend); //多空组合判断是否已到到期日且无持仓信息 @@ -490,18 +490,20 @@ namespace YLErp.Modules.SwapModule } var eodPosition = eodPositions.FirstOrDefault(x => x.PositionId == interest.id);//上一日日终利息信息 可能不存在 var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == interest.id);//当前结算日日终利息信息 - var insterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);//自动互换观察日信息 + var observationInterval = FindObservationInterval(interest, settleDate);//自动互换观察日信息 List dealInterests = new List(); dealInterests.AddRange(flowEvents); var dealInterest = dealInterests.FirstOrDefault(n => n.PositionId == interest.id);//当日是否做过互换或平仓 var swapEvents = flowEvents.Where(x => (x.EventType == (int)SwapEventTypeEnum.互换 || x.EventType == (int)SwapEventTypeEnum.平仓) && x.PositionId == interest.id).ToList(); //如果当日有互换/当日有平仓 不再重新生成或更新 - Log.Info($"insterval is {insterval},hasSwap is {hasSwap},hasClose is {hasClose}"); - if (insterval != null && !hasSwap) + Log.Info($"observationInterval is {observationInterval},hasSwap is {hasSwap},hasClose is {hasClose}"); + // 分派优先级与粒度说明见 ResolveInterestScenario;8 组合表驱动覆盖见 InterestEodScenarioDispatchTest。 + // 仅观察日两个分支把返回值收进 autoInterests(→资金记录)——分派错序=静默少结。 + if (observationInterval != null && !hasSwap) { if (!hasClose)//当日无平仓 { - var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv); + var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv); if (_autoInterests.Count > 0) { autoInterests.AddRange(_autoInterests); @@ -509,7 +511,7 @@ namespace YLErp.Modules.SwapModule } else { - var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiTotalNotional, swapEvents, closeNational, true, grossPrice, orginPv); + var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: true, grossPrice, orginPv); if (_autoInterests.Count > 0) { autoInterests.AddRange(_autoInterests); @@ -522,7 +524,7 @@ namespace YLErp.Modules.SwapModule } else if (hasClose) { - SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiTotalNotional, swapEvents, closeNational, false, grossPrice, orginPv); + SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: false, grossPrice, orginPv); } else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息 { @@ -530,6 +532,46 @@ namespace YLErp.Modules.SwapModule } } } + + /// + /// 利息腿 EOD 归档场景分派(纯函数;表驱动单测见 InterestEodScenarioDispatchTest)。 + /// 优先级链(承重业务语义,不能乱序): + /// ① hasSwap(手工互换) → 按事件流水重新生成,压制观察日自动结息 + /// ② observationInterval(观察日) 存在 → 自动结息;同日有平仓 → autoSwap=true + /// ③ hasClose(纯平仓, 非观察日) → autoSwap=false + /// ④ 普通日 → 复制上一日终并计提当日新增 + /// 注意:hasSwap/hasClose 是交易级标志(整笔合约当天有无事件), + /// observationInterval 是腿级(本条利息腿当天是否观察日)——粒度不同,分派依赖此区分。 + /// + public enum InterestEodScenario + { + ManualSwap, // ① 手工互换:压制观察日自动结息 + AutoSettleWithClose, // ② 观察日 + 当日平仓 (autoSwap=true) + AutoSettle, // ② 观察日 + 当日无平仓 + CloseOnly, // ③ 非观察日 + 当日平仓 (autoSwap=false) + RollForward, // ④ 普通日滚动 + } + + protected static InterestEodScenario ResolveInterestScenario(bool hasInterval, bool hasSwap, bool hasClose) + { + if (hasSwap) + return InterestEodScenario.ManualSwap; + if (hasInterval) + return hasClose ? InterestEodScenario.AutoSettleWithClose : InterestEodScenario.AutoSettle; + if (hasClose) + return InterestEodScenario.CloseOnly; + return InterestEodScenario.RollForward; + } + + /// + /// 查找利息腿在指定结算日的观察日信息(SwapIntervalList 中 Date==settleDate 且 Settlement==1 的记录)。 + /// 观察日即自动结息触发日;返回 null 表示当日非观察日。分派见 ResolveInterestScenario。 + /// + protected static IntervalModel FindObservationInterval(swap_position interest, DateTime settleDate) + { + return interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1); + } + /// /// 处理浮动腿归档 /// @@ -1322,7 +1364,7 @@ namespace YLErp.Modules.SwapModule var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true; // 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。 // 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。 - // 恒1 重算的 InterestAmount 是不进结算现金流的中间值;系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。 + // 恒1 重算的 InterestAmount 是结算现金流的直接输入(非无害中间值):系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。 // 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着 // SwapCalcTrace 分段过程日志追计算;autoSwap=观察日结现路径。 Log.Info($"[EOD平仓后收盘结息] tradeId={td.id} valueDate={valueDate:yyyy-MM-dd} autoSwap={autoSwap} " +