diff --git a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs index 237cf1b0..fa37af8e 100644 --- a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs +++ b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs @@ -66,5 +66,23 @@ namespace YLErp.Modules.EodModule Assert.AreEqual(204000m, actual); } + + [TestMethod] + public void CalcPayment_CorporateActionUsesPreCorporateActionQuantity() + { + var payments = new List + { + new BondPayment { payment_interest = 2m }, + new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true } + }; + var method = typeof(BondPaymentService).GetMethod( + nameof(BondPaymentService.CalcPayment), + new[] { typeof(List), typeof(decimal), typeof(decimal), typeof(decimal), typeof(decimal?) }); + + Assert.IsNotNull(method, "公司行为现金分红需要支持单独传入除权前数量。"); + var actual = (decimal)method.Invoke(CreateService(), new object[] { payments, 2000m, 1m, 1m, (decimal?)1000m }); + + Assert.AreEqual(1040m, actual, "原生付息按当前 2000 份计算为 40,公司行为分红按除权前 1000 份计算为 1000。"); + } } } diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index 5b6f03ce..fe12ab75 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -189,10 +189,26 @@ namespace YLErp.Modules.EodModule decimal qty, decimal longRatio, decimal payDirection) + { + return CalcPayment(payments, qty, longRatio, payDirection, null); + } + + /// + /// 公司行为现金分红按登记日权益数量计算;同一窗口中的原生债券付息仍按当前持仓数量计算。 + /// + public decimal CalcPayment( + List payments, + decimal qty, + decimal longRatio, + decimal payDirection, + decimal? corporateActionQty) { var actualAmount = (payments ?? new List()).Sum(payment => { - var paymentAmount = (payment.payment_interest ?? 0m) * qty; + var paymentQty = payment.IsCorporateActionCashDividend + ? corporateActionQty ?? qty + : qty; + var paymentAmount = (payment.payment_interest ?? 0m) * paymentQty; // bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的 // 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。 return payment.IsCorporateActionCashDividend diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index d9a05856..1d2b01d7 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -357,6 +357,19 @@ namespace YLErp.Modules.SwapModule directionRatio); } + protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, + decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty) + { + if (!corporateActionQty.HasValue) + { + return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); + } + + var service = new BondPaymentService(UserInfo); + var payments = service.GetBondPayments(underlyingCode, fromDate, toDate); + return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty); + } + // ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)---- /// 查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表) @@ -611,6 +624,11 @@ namespace YLErp.Modules.SwapModule var corporateActionBeforePositions = BuildCorporateActionBeforePositions( eodPositions, // 上一日终持仓 posiList); + var corporateActionCashDividendBeforePositions = corporateActionBeforePositions + .Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode) + && exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend) + && dividend.GiveCashAmount != 0m) + .ToList(); // 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。 // 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。 @@ -627,7 +645,8 @@ namespace YLErp.Modules.SwapModule settleDate, // 收盘日期 td, // 交易 preSettleDate, // 上一交易日 - flowEvents); // 流水事件 + flowEvents, // 流水事件 + corporateActionCashDividendBeforePositions); // 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment // 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。 @@ -1435,7 +1454,8 @@ namespace YLErp.Modules.SwapModule DateTime settleDate, trade td, DateTime preSettleDate, - List flowEvents) + List flowEvents, + IReadOnlyCollection corporateActionBeforePositions = null) { string settleDateStr = settleDate.ToString("yyyy-MM-dd"); string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd"); @@ -1457,18 +1477,20 @@ namespace YLErp.Modules.SwapModule var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息 var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息 var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id); + var corporateActionBeforeQuantity = corporateActionBeforePositions? + .FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity; eod_swap_position eodPosi = new eod_swap_position(); if (eodPosition == null) { - eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents); + eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity); } else if (unwindEvents.Count() == 0) { - eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate); + eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity); } else { - eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents); + eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity); } Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}"); list.Add(eodPosi); @@ -2626,7 +2648,7 @@ namespace YLErp.Modules.SwapModule /// 当日日终归档信息 /// 当日平仓/互换事件信息 /// 交易信息 - protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate) + protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, decimal? corporateActionBeforeQuantity = null) { if (curretEod == null) { @@ -2648,7 +2670,7 @@ namespace YLErp.Modules.SwapModule decimal tax = um.ValueAddedTax ?? 0; if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0) { - decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax); } curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0; @@ -2710,7 +2732,7 @@ namespace YLErp.Modules.SwapModule /// /// /// - protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents) + protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents, decimal? corporateActionBeforeQuantity = null) { if (curretEod == null) { @@ -2744,7 +2766,7 @@ namespace YLErp.Modules.SwapModule // 修改,互换事件会影响待实现的分红的,现在要算上 if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0)) { - decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax); } curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; @@ -2871,7 +2893,8 @@ namespace YLErp.Modules.SwapModule /// /// /// - protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List unwindEvents) + protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, + DateTime preSettleDate, List unwindEvents, decimal? corporateActionBeforeQuantity = null) { eod_swap_position curretEod = new eod_swap_position(); var um = GetUnderlyingData(position.UnderlyingCode); @@ -2924,7 +2947,7 @@ namespace YLErp.Modules.SwapModule if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0) { decimal tax = um.ValueAddedTax ?? 0; - decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio); + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); payment = DividendCalc.AfterTax(payment, tax); //var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红 curretEod.TdPosiDividend = payment;