diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs index ccf6c195..f0493882 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs @@ -317,6 +317,98 @@ namespace YLErp.Modules.SwapModule "100% 平仓: 应返还本金应=Fix=9,180,000(closePercent=1 时指数 bug 不显现,须保持正确)"); } + // ---- 非预付金腿(标的期初全价=9)同样验证:证明修复对所有"单利盘中"腿通用且正确 ---- + // CalcDailySimpleInterest 是所有单利腿(InterestType=0)的盘中计息通用函数,非预付金专用。 + // 用户关切:修复会否波及非预付金腿?结论—— + // · closePercent=1(日常计息/全平)时 1^N=1=1^1,修复前后逐位恒等,零影响; + // · closePercent<1(部分平仓)时,所有单利腿此前都被同一 bug 指数级缩小,修复后统一为 + // 正确的线性缩放(平仓 X% => 本金×X),这是修正而非破坏。 + // 本组用非预付金腿(标的期初全价=9,orginPv 不被对齐为 Fix、走交易名义本金)独立复现并锁定。 + private const decimal NonPrepayNotional = 1_000_000m; + + private swap_flow_event CalcUnwindMultiDayNonPrepay(decimal closePercent, decimal notional = NonPrepayNotional, + int restDays = 7, decimal rate = 0m) + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", // 算头不算尾(与生产一致) + SettlementRules = 0 + }) + }; + var td = new trade + { + id = 1, TradeNumber = "UT-NONPREPAY-EXP", ClientId = 999998, + TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart, + ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", + StockEqvNotional = (double)notional, Notional = (double)notional, + trade_extend = extend + }; + var position = new swap_position + { + id = 2002, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, // 非预付金腿(=9):orginPv 不会被对齐为 Fix + InterestRateDefault = rate, InterestPrincipalFix = 0m, + PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1), + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = restDays, + interest_rule = 0, FloatRateUnderlyingCode = null, + InterestSwapInterval = "[]" + }; + var eod = new List + { + new eod_swap_position + { + id = 8, SwapTradeId = 1, PositionId = 2002, + ValueDate = ProdEodValueDate, + TdInterestPrincipal = notional, // 计息基数=名义本金 → dynomicPrincipal = eodTd + notional - orginPv = notional + PosiNotionalValue = notional, + InterestProfitSum = 0m, FloatRate = 0m + } + }; + // orginPv 传 notional:非预付金腿不走 877-881 的 Fix 对齐,dynomicPrincipal = notional + notional - notional = notional + var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate, + eod, new List { position }, + notional, notional, notional, notional * closePercent, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, notional, false, settment: false, newCalcLast: false, closeList: null); + Assert.AreEqual(1, interests.Count, "非预付金腿应生成 1 条 flow_event"); + return interests[0]; + } + + [TestMethod] + public void 非预付金腿_部分平仓50_盘中重置周期7天_应线性缩放不受指数bug影响() + { + var fe = CalcUnwindMultiDayNonPrepay(0.5m); + Console.WriteLine($"[TDD][非预付金50%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=7,812.5, 期望=500,000)"); + // 正确:N×0.5=500,000。buggy:N×0.5^7=7,812.5(同一指数 bug,证明非预付金腿此前也中招)。 + Assert.AreEqual(500_000m, fe.InterestPrincipal, + "非预付金腿(标的期初全价) 50% 平仓应=名义本金×0.5=500,000,不应被 closePercent^7 缩小"); + } + + [TestMethod] + public void 非预付金腿_部分平仓10_盘中重置周期7天_应线性缩放不受指数bug影响() + { + var fe = CalcUnwindMultiDayNonPrepay(0.1m); + Console.WriteLine($"[TDD][非预付金10%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=0.1, 期望=100,000)"); + Assert.AreEqual(100_000m, fe.InterestPrincipal, + "非预付金腿(标的期初全价) 10% 平仓应=名义本金×0.1=100,000,不应被 closePercent^7 缩小"); + } + + [TestMethod] + public void 非预付金腿_全平_修复前后恒等_零影响() + { + // closePercent=1 时 1^N=1=1^1:这是"修复不波及非平仓/全平计息"的数学不变量证明。 + var fe = CalcUnwindMultiDayNonPrepay(1m); + Console.WriteLine($"[TDD][非预付金100%] 实测 InterestPrincipal={fe.InterestPrincipal} (期望=1,000,000)"); + Assert.AreEqual(NonPrepayNotional, fe.InterestPrincipal, + "非预付金腿 全平应=名义本金(closePercent=1 时修复前后恒等,日常计息/全平零影响)"); + } + [TestMethod] public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金() {