diff --git a/UnitTestProject/Modules/SwapModule/FundCorporateActionRollbackAndUnwindTest.cs b/UnitTestProject/Modules/SwapModule/FundCorporateActionRollbackAndUnwindTest.cs index 1f3a6d93..e8d805f4 100644 --- a/UnitTestProject/Modules/SwapModule/FundCorporateActionRollbackAndUnwindTest.cs +++ b/UnitTestProject/Modules/SwapModule/FundCorporateActionRollbackAndUnwindTest.cs @@ -198,6 +198,41 @@ namespace YLErp.Modules.SwapModule "Fund 份额缩小为 0.01 倍时,直接平仓期初价应为 100 / 0.01 = 10000"); } + [TestMethod] + public void FCA_UW_009_登记日跨非交易日到生效日按范围恢复基金基线() + { + var eodDate = new DateTime(2026, 7, 12); + var effectiveDate = new DateTime(2026, 7, 13); + var unwindDate = new DateTime(2026, 7, 17); + var realtime = CreateRealtimeFundPosition(); + var eod = CreateEod(eodDate, 1000m, 100m); + var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false); + service.ExDividendInfos.Add(new ex_dividend_info + { + id = 1, + UnderlyingCode = "FUND.TEST", + // 7/10 登记,7/13 生效;7/11、7/12 虽无交易但仍可能存在未除权 EOD 快照。 + ExDividendDate = new DateTime(2026, 7, 10), + EffectiveDate = effectiveDate, + // 生产数据口径:1 拆 2 直接存 Split=2,GiveShareAmount 不参与该拆分。 + GiveShareAmount = 0m, + Split = 2m, + ValidStatus = true + }); + var unwindData = CreateFullCloseUnwindData(); + unwindData.ValueDate = unwindDate; + unwindData.UnwindDate = unwindDate; + + Assert.IsTrue(service.RestoreEffectiveFundPositionForTest(unwindData, unwindDate)); + + Assert.AreEqual(2000m, realtime.PosiQuantity, + "7 月 17 日平仓应补应用 7 月 13 日生效的 Split=2,公司行为不能只按平仓日命中"); + Assert.AreEqual(50m, realtime.PosiGrossPrice); + Assert.AreEqual(2000m, unwindData.PositionQty); + Assert.AreEqual(2000m, unwindData.CloseQty); + Assert.AreEqual(50m, unwindData.FlowEvents.Single().PosiGrossPrice); + } + [TestMethod] public void FCA_UW_003_正式平仓按FundEod基线重算PnL和现金() { diff --git a/UnitTestProject/Modules/SwapModule/TestableSwapDealService.cs b/UnitTestProject/Modules/SwapModule/TestableSwapDealService.cs index 450c52d7..dcfde1d7 100644 --- a/UnitTestProject/Modules/SwapModule/TestableSwapDealService.cs +++ b/UnitTestProject/Modules/SwapModule/TestableSwapDealService.cs @@ -60,6 +60,20 @@ namespace YLErp.Modules.SwapModule && x.UnderlyingCode == underlyingCode && x.EffectiveDate == valueDate.Date); + protected override List FindFundCorporateActions( + string underlyingCode, + DateTime eodDate, + DateTime valueDate) + => ExDividendInfos + .Where(x => x.ValidStatus + && x.UnderlyingCode == underlyingCode + && x.EffectiveDate.HasValue + && x.EffectiveDate.Value.Date > eodDate.Date + && x.EffectiveDate.Value.Date <= valueDate.Date) + .OrderBy(x => x.EffectiveDate) + .ThenBy(x => x.id) + .ToList(); + protected override decimal GetFundCorporateActionClosePrice( ex_dividend_info dividendInfo, decimal fallbackPrice) diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index c4b56206..d5f5f45f 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -98,7 +98,8 @@ namespace YLErp.Modules.EodModule /// public List GetBondPayments(string underlyingCode, DateTime startDate, DateTime endDate) { - var result = DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode && x.payment_date > startDate && x.payment_date <= endDate).AsNoTracking().ToList(); + var result = DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode + && x.payment_date > startDate && x.payment_date <= endDate).AsNoTracking().ToList(); // 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。 // 口径约定:bond_payment_info.payment_interest 对 Stock/Fund 统一按“每 10 份派现金额”存储, diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 4b9daa62..cef8c391 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -310,6 +310,29 @@ namespace YLErp.Modules.SwapModule && x.EffectiveDate.Value == valueDate.Date); } + /// + /// 查询从最近 EOD 之后到平仓日已经生效的 Stock/Fund 公司行为。 + /// 平仓可能跨越登记日、生效日和多个非交易日,因此不能只按 valueDate 命中一条记录。 + /// 调用方已限定为 Stock/Fund 浮动腿;同日多条记录按生效日、主键稳定排序后逐条应用。 + /// + protected virtual List FindFundCorporateActions( + string underlyingCode, + DateTime eodDate, + DateTime valueDate) + { + var fromDate = eodDate.Date; + var toDate = valueDate.Date; + return DbContext.ex_dividend_info + .Where(x => x.ValidStatus + && x.UnderlyingCode == underlyingCode + && x.EffectiveDate.HasValue + && x.EffectiveDate.Value.Date > fromDate + && x.EffectiveDate.Value.Date <= toDate) + .OrderBy(x => x.EffectiveDate) + .ThenBy(x => x.id) + .ToList(); + } + /// /// Stock/Fund 公司行为系数仍使用登记日收盘价,而不是生效日盘中/收盘价。 /// 测试可用 EOD 快照价格作为回退值;生产从登记日行情表取真实收盘价。 @@ -387,13 +410,12 @@ namespace YLErp.Modules.SwapModule } // 最近 EOD 已经处于生效日或更晚时,说明该快照本身已经是除权后基线, - // 不能再次套系数。只有“最近 EOD < EffectiveDate <= valueDate”时, - // 才在盘中恢复后补一次公司行为。 - var corporateAction = FindFundCorporateAction( + // 不能再次套系数。若平仓跨过多个生效日,则按生效日、id 顺序逐条补齐。 + var corporateActions = FindFundCorporateActions( position.UnderlyingCode, + eodPosition.ValueDate, valueDate); - if (corporateAction?.EffectiveDate > eodPosition.ValueDate.Date - && corporateAction.EffectiveDate.Value.Date <= valueDate.Date) + foreach (var corporateAction in corporateActions ?? new List()) { var closePrice = GetFundCorporateActionClosePrice( corporateAction,