From 392e36a8205db2ce452300b551d8bda49dddab97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 21 Aug 2026 16:51:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(bond):=20=E6=B7=BB=E5=8A=A0=E5=85=AC?= =?UTF-8?q?=E5=8F=B8=E8=A1=8C=E4=B8=BA=E7=8E=B0=E9=87=91=E5=88=86=E7=BA=A2?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=98=E6=81=AF?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 BondPayment 模型中添加 IsCorporateActionCashDividend 属性用于区分不同数据来源 - 修改 BondPaymentService 中的 CalcPayment 方法,按每条记录来源分别处理金额单位转换 - 更新单元测试以验证混合债券付息和公司行为分红的正确计算 - 移除不再需要的 useBondPriceScale 参数简化方法接口 - 修正注释说明以反映新的按记录类型分别计算的逻辑 --- Framework/YLErp.Core/DBModels/BondPayment.cs | 8 ++++ .../BondPaymentServiceCalculationTest.cs | 27 ++++++++++-- .../Modules/EodModule/BondPaymentService.cs | 41 ++++++++----------- .../Modules/SwapModule/SwapDealService.cs | 11 ++--- .../SwapModule/SwapEodPositionService.cs | 14 ++----- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/Framework/YLErp.Core/DBModels/BondPayment.cs b/Framework/YLErp.Core/DBModels/BondPayment.cs index 3de640b4..58838d6e 100644 --- a/Framework/YLErp.Core/DBModels/BondPayment.cs +++ b/Framework/YLErp.Core/DBModels/BondPayment.cs @@ -112,5 +112,13 @@ namespace YLErp.DBModels [DisplayName("更新时间")] [Column("update_time")] public DateTime? update_time { get; set; } + + /// + /// 是否由 ex_dividend_info 在内存中补充的 Stock/Fund 公司行为现金分红。 + /// bond_payment_info 原生记录的 paying_interest 按每 100 份存储;公司行为记录的 + /// GiveCashAmount 按每 10 份存储。该标记只在运行时区分两种金额单位,不落库。 + /// + [NotMapped] + public bool IsCorporateActionCashDividend { get; set; } } } diff --git a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs index 822de107..237cf1b0 100644 --- a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs +++ b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs @@ -29,11 +29,11 @@ namespace YLErp.Modules.EodModule } [TestMethod] - public void CalcPayment_StockOrFundDividend_DoesNotApplyBondScale() + public void CalcPayment_CorporateActionDividend_UsesPerTenScale() { var payments = new List { - new BondPayment { payment_interest = 10m } + new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true } }; // GiveCashAmount=10(每 10 份派 10)时,payment_interest 直接存 10; @@ -42,10 +42,29 @@ namespace YLErp.Modules.EodModule payments, qty: 1000m, longRatio: 1m, - payDirection: 1m, - useBondPriceScale: false); + payDirection: 1m); Assert.AreEqual(1000m, actual); } + + [TestMethod] + public void CalcPayment_FundMixedBondPaymentAndCorporateAction_UsesEachRecordScale() + { + var payments = new List + { + // bond_payment_info:每 100 份付 2,200000 份应为 4000。 + new BondPayment { payment_interest = 2m }, + // ex_dividend_info:每 10 份派 10,200000 份应为 200000。 + new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true } + }; + + var actual = CreateService().CalcPayment( + payments, + qty: 200000m, + longRatio: 1m, + payDirection: 1m); + + Assert.AreEqual(204000m, actual); + } } } diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index e71c2e6a..19366db5 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -144,7 +144,7 @@ namespace YLErp.Modules.EodModule // 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。 // Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值, - // 最后的 /10 由 CalcPayment 非债券分支完成。 + // 最后的 /10 只应用于这类内存补充记录;bond_payment_info 原生记录仍按每 100 份处理。 var corporatePayments = (from dividend in DbContext.ex_dividend_info.AsNoTracking() join underlying in DbContext.underlying_manager.AsNoTracking() on dividend.UnderlyingCode equals underlying.UnderlyingCode @@ -169,7 +169,8 @@ namespace YLErp.Modules.EodModule payment_interest = paymentInterest, paying_price = paymentInterest, create_time = dividend.OptDate, - update_time = dividend.OptDate + update_time = dividend.OptDate, + IsCorporateActionCashDividend = true }); } return result; @@ -205,43 +206,35 @@ namespace YLErp.Modules.EodModule DateTime endDate, decimal qty, decimal longRatio, - decimal payDirection, - bool useBondPriceScale = true) + decimal payDirection) { var payments = GetBondPayments(underlyingCode, startDate, endDate); - return CalcPayment(payments, qty, longRatio, payDirection, useBondPriceScale); + return CalcPayment(payments, qty, longRatio, payDirection); } /// - /// 计算某标的期间现金流。债券与 Stock/Fund 公司行为共用 bond_payment_info, - /// 但通过 useBondPriceScale 明确区分两种入库金额单位。 + /// 计算某标的期间现金流。债券期间付息与 Stock/Fund 公司行为现金分红可能同时命中, + /// 必须按每条记录的来源单位分别计算,不能按标的类型对整个集合统一除以 10 或 100。 /// /// 期间付息集合 /// 持仓数量 /// 多空方向 /// 收支方向 - /// - /// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息, - /// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红 - /// 在 bond_payment_info 中按“每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金, - /// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。 - /// /// public decimal CalcPayment( List payments, decimal qty, decimal longRatio, - decimal payDirection, - bool useBondPriceScale = true) + decimal payDirection) { - var interest = payments.Sum(s => s.payment_interest ?? 0); - var paymentAmount = interest * qty; - // 债券:interest 为每 100 元面值的票息,×qty 后需 ÷100 转为实际金额。 - // Fund/Stock 公司行为:payment_interest 存的是“每 10 份派现金额”(GiveCashAmount 原值), - // interest × qty 得到“每 10 份派现额 × 持仓份数”,需再 ÷10 才是实际现金; - // 既不能套债券的 /100,也不能直接返回 paymentAmount(那样会放大 10 倍)。 - var actualAmount = useBondPriceScale - ? BondPriceConverter.ToStorage(paymentAmount) - : paymentAmount / 10; + var actualAmount = (payments ?? new List()).Sum(payment => + { + var paymentAmount = (payment.payment_interest ?? 0m) * qty; + // bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的 + // 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。 + return payment.IsCorporateActionCashDividend + ? paymentAmount / 10m + : BondPriceConverter.ToStorage(paymentAmount); + }); return actualAmount * longRatio * payDirection; } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index e42d0602..d8e08c7d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -2156,18 +2156,13 @@ namespace YLErp.Modules.SwapModule int shortRatio = DirectionRatio.LongShort(flowEvent.PositionType); int directionRatio = DirectionRatio.ReceivePay(flowEvent.PayDirection); var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode); - // 债券付息按每百元票息存储,继续走 BondPriceConverter;Stock/Fund 的公司行为 - // 现金分红按每 10 份金额存储,实际现金 = payment_interest * qty / 10,不能 /100。 - // 标的资料缺失时保持旧债券口径,避免未知标的的历史平仓金额被放大。 - var useBondPriceScale = um == null - || !SwapEodPositionService.IsCorporateActionInstrument(um.UnderlyingInstrumentType); - // + 付息日>上日日终且小于等于平仓日期的分红数据 + // 期间付息和公司行为现金分红可能同时命中;BondPaymentService 逐条按来源单位换算, + // 原生 bond_payment_info 记录按每 100 份,公司行为表补充记录按每 10 份。 var dividendIn = servie.CalcPayment( payments, unwindQty, shortRatio, - directionRatio, - useBondPriceScale); + directionRatio); decimal tax = um?.ValueAddedTax ?? 0; dividendIn = DividendCalc.AfterTaxRaw(dividendIn, tax); diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 5b7944db..9650d66d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -342,25 +342,19 @@ namespace YLErp.Modules.SwapModule /// /// 计算期间现金流(生产: BondPaymentService;测试: 返回固定值)。 - /// BondPaymentService 的默认仍是债券百分比价格口径;TRS Stock/Fund 的公司行为 - /// 分红行按每 10 份金额入库,因此必须显式关闭 BondPriceConverter 的 /100 换算。 - /// 标的资料缺失时沿用债券口径,避免把未知历史数据放大 100 倍。 + /// BondPaymentService 会按每条付款记录的数据来源换算:bond_payment_info 原生期间付息 + /// 按每 100 份,公司行为表补充的现金分红按每 10 份。不能只按 Fund/Stock 标的类型 + /// 选择一个统一除数,否则两类记录同时命中时会错算。 /// protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) { - var underlying = GetUnderlyingData(underlyingCode); - // 本期现金分红只覆盖 TRS Stock/Fund。其他非债券(期货、期权等)虽然也不属于 - // 债券,但尚未接入本现金分红表,继续使用默认债券换算,避免扩大改造范围。 - var useBondPriceScale = underlying == null - || !IsCorporateActionInstrument(underlying.UnderlyingInstrumentType); return new BondPaymentService(UserInfo).CalcPayment( underlyingCode, fromDate, toDate, qty, shortRatio, - directionRatio, - useBondPriceScale); + directionRatio); } // ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----