From 58bd27c5968a39667a45159deccbaa374f0f74eb Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 14 Aug 2026 14:24:23 +0800 Subject: [PATCH] =?UTF-8?q?test(EQD-7004):=20=E8=A1=A5=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=B9=B3=E4=BB=93=E8=B7=AF=E5=BE=84=E5=A4=9A=E6=AC=A1=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=E4=B8=8D=E9=87=8D=E5=A4=8D=E8=AE=A1?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E5=AE=9E=E8=AF=81=E6=B5=8B=E8=AF=95=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=A9=B1=E5=8A=A8=E7=9C=9F=E5=AE=9E=20BondPaymentServ?= =?UTF-8?q?ice.CalcPayment(reg=5Fdate=20=E5=8F=A3=E5=BE=84=C3=97=E5=BD=93?= =?UTF-8?q?=E6=AC=A1=E5=B9=B3=E4=BB=93=E9=87=8F=20unwindQty)=EF=BC=8C?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=80=BB=E9=A2=9D=E6=8C=89=E7=99=BB=E8=AE=B0?= =?UTF-8?q?=E6=97=A5=E6=8C=81=E4=BB=93=E5=88=86=E6=91=8A=E3=80=81=E4=B8=8D?= =?UTF-8?q?=E8=87=AA=E6=B4=BD=E5=A4=9A=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoUnwindMultiPartialDividendTest.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/AutoUnwindMultiPartialDividendTest.cs diff --git a/UnitTestProject/Modules/SwapModule/AutoUnwindMultiPartialDividendTest.cs b/UnitTestProject/Modules/SwapModule/AutoUnwindMultiPartialDividendTest.cs new file mode 100644 index 00000000..ea5035a9 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/AutoUnwindMultiPartialDividendTest.cs @@ -0,0 +1,69 @@ +using YLErp.Modules.EodModule; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 自动平仓路径(AuotoSwapUnwind → EnrichDividendIn, SwapDealService.cs:1668-1687)多次部分平仓是否多算的实证。 + /// EnrichDividendIn 核心:GetBondPayments(td.StartDate, closeDate) × unwindQty(当次平仓量,非剩余持仓)。 + /// 本测试直接驱动真实 BondPaymentService.CalcPayment(与 EnrichDividendIn 等价:GetBondPayments 按 reg_date 过滤 + CalcPayment × unwindQty), + /// 内存注入 reg_date 数据,不连库。完整 AuotoSwapUnwind 链路因 EnrichDividendIn 直接 new BondPaymentService 查库、无内存 seam 注入点,故用计算核心等价验证。 + /// + /// 结论验证:多次跨越登记日的部分平仓,每次 × 当次平仓量 → 总额 = 各批按登记日持有 × 平仓量分摊, + /// 不自洽多算、不重复计入重叠窗口。 + /// (纠正此前"从建仓日重算导致重复计入"的推断:该推断误以为 CalcPayment 乘剩余持仓,实际乘当次 unwindQty。) + /// + [TestClass] + public class AutoUnwindMultiPartialDividendTest + { + private const string BondCode = "230004.IB"; + private static readonly DateTime StartDate = new(2026, 1, 5); + private static readonly DateTime Reg1 = new(2026, 5, 15); // 每百元付息 10 + private static readonly DateTime Reg2 = new(2026, 6, 15); // 每百元付息 12 + + private sealed class BridgeBps : BondPaymentService + { + public BridgeBps(OptUserInfo u) : base(u) { } + protected override IQueryable QueryBondPayments(string underlyingCode) + => new List + { + new BondPayment { underlyingCode = BondCode, reg_date = Reg1, payment_date_pl = Reg1, payment_date = Reg1, payment_interest = 10m }, + new BondPayment { underlyingCode = BondCode, reg_date = Reg2, payment_date_pl = Reg2, payment_date = Reg2, payment_interest = 12m }, + }.Where(x => x.underlyingCode == underlyingCode).AsQueryable(); + } + + // 等价于 EnrichDividendIn 的数值核心:GetBondPayments(StartDate, closeDate) × unwindQty + private static decimal EnrichOnce(DateTime closeDate, decimal unwindQty) + { + var svc = new BridgeBps(OptUserInfo.UnitTestUser); + return svc.CalcPayment(BondCode, StartDate, closeDate, unwindQty, 1, 1); + } + + [TestMethod] + public void 多次部分平仓_自动路径总额按登记日持仓分摊_不自洽多算() + { + decimal totalFace = 10_000m; // 总面额 1 万元 + decimal halfFace = totalFace / 2m; // 每次平一半 + + // 第一次 5/20 平一半:窗口(Start,5/20] 仅含 reg1 → 10 × 5000/100 = 500 + var d1 = EnrichOnce(new DateTime(2026, 5, 20), halfFace); + // 第二次 6/20 平一半:窗口(Start,6/20] 含 reg1+reg2 → (10+12) × 5000/100 = 1100 + var d2 = EnrichOnce(new DateTime(2026, 6, 20), halfFace); + var total = d1 + d2; + + // 经济应得(登记日持有规则): + // 第一批5000元:5/15持有✓(10)、6/15未持有✗ → 10×5000/100 = 500 + // 第二批5000元:5/15持有✓(10)、6/15持有✓(12) → 22×5000/100 = 1100 + decimal expected = 10m * halfFace / 100m + (10m + 12m) * halfFace / 100m; + + Assert.AreEqual(500m, d1, 0.001m, "第一次(5/20)只含 reg1 = 500"); + Assert.AreEqual(1100m, d2, 0.001m, "第二次(6/20)含 reg1+reg2 = 1100"); + Assert.AreEqual(expected, total, 0.001m, + "两次部分平仓总额 = 按登记日持有×平仓量分摊的应得值,重叠窗口不重复计同量(纠正:乘当次 unwindQty 而非剩余持仓)"); + + // 反证:若手动路径口径(第一次平仓即给全量待实现 = 两次分红×总面额)会多算 + decimal manualFullIfFirst = (10m + 12m) * totalFace / 100m; // 2200 + Assert.IsTrue(manualFullIfFirst > total, + "反证:手动全量落袋口径(2200) > 自动分摊口径(1600),多算方是手动路径而非自动路径"); + } + } +}