feat(bond): 支持股票和基金现金分红纳入债券支付计算 - 收紧过度开发行为

- 实现股票/基金现金分红数据从 ex_dividend_info 同步到 BondPayment
- 新增公司行为去重机制,避免镜像任务完成后重复计息
- 统一现金分红存储口径为"每 10 份派现金额",保持与同步任务一致性
- 修改 CalcPayment 方法,股票/基金分红需除以 10 转换实际现金金额
- 添加单元测试验证债券票息和股票/基金分红的不同计算方式
- 更新文档注释说明"每 10 份派现金额"存储规范
- 修复公司行为生效日处理逻辑,确保正确应用除权系数
- 扩展测试覆盖股票类证券的公司行为处理场景
This commit is contained in:
张名锐
2026-08-20 13:30:36 +08:00
parent aa3548e77f
commit b2f4e16782
13 changed files with 384 additions and 368 deletions
@@ -1,4 +1,4 @@
using BaseOUDAL;
using BaseOUDAL;
using DocumentFormat.OpenXml.Bibliography;
using ExcelDataReader.Log;
using YLErp.DBModels;
@@ -99,6 +99,52 @@ namespace YLErp.Modules.EodModule
public List<BondPayment> 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();
// 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。
// 口径约定:bond_payment_info.payment_interest 对 Stock/Fund 统一按“每 10 份派现金额”存储,
// 即直接存 ex_dividend_info.GiveCashAmount 原值,不做 /10
// 最后的 /10 由 CalcPayment 非债券分支完成。
// 去重:镜像任务完成后的正式记录带 jsid = -dividend.id;此处先按该负号标记,
// 或用“同一实际付息日 + 同一派现金额”兜底去重,避免镜像完成后重复计息。
var corporatePayments = (from dividend in DbContext.ex_dividend_info.AsNoTracking()
join underlying in DbContext.underlying_manager.AsNoTracking()
on dividend.UnderlyingCode equals underlying.UnderlyingCode
where dividend.ValidStatus
&& dividend.EffectiveDate.HasValue
&& dividend.EffectiveDate.Value > startDate
&& dividend.EffectiveDate.Value <= endDate
&& dividend.GiveCashAmount != 0
&& dividend.UnderlyingCode == underlyingCode
&& (underlying.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Stock
|| underlying.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
select dividend).ToList();
foreach (var dividend in corporatePayments)
{
// 按“每 10 份派现金额”口径,直接存 GiveCashAmount 原值,与同步任务/CalcPayment 保持一致。
var paymentInterest = dividend.GiveCashAmount;
var hasMirroredPayment = result.Any(payment =>
payment.jsid == -dividend.id
|| (payment.payment_date.HasValue
&& payment.payment_date.Value.Date == dividend.EffectiveDate.Value.Date
&& payment.payment_interest == paymentInterest));
if (hasMirroredPayment)
{
continue;
}
result.Add(new BondPayment
{
underlyingCode = dividend.UnderlyingCode,
payment_date_pl = dividend.EffectiveDate,
payment_date = dividend.EffectiveDate,
payment_interest = paymentInterest,
paying_price = paymentInterest,
channel_source = ExDividendDataSources.Manual,
jsid = -dividend.id,
create_time = dividend.OptDate,
update_time = dividend.OptDate
});
}
return result;
}
@@ -142,8 +188,8 @@ namespace YLErp.Modules.EodModule
/// <param name="useBondPriceScale">
/// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息,
/// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红
/// 在 bond_payment_info 中按每 10 份存储,payment_interest * qty 已经是实际现金,
/// 不能再做一次 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
/// 在 bond_payment_info 中按每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金,
/// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
/// </param>
/// <returns></returns>
public decimal CalcPayment(
@@ -156,11 +202,12 @@ namespace YLErp.Modules.EodModule
var interest = payments.Sum(s => s.payment_interest ?? 0);
var paymentAmount = interest * qty;
// 债券:interest 为每 100 元面值的票息,×qty 后需 ÷100 转为实际金额。
// Fund/Stock 公司行为:interest 已由【同步任务】写成 GiveCashAmount/10
// ×qty 就是“每 10 份派现额 × 持仓份”,必须保留原金额,不能套债券的 /100。
// Fund/Stock 公司行为:payment_interest 存的是“每 10 份派现金额”(GiveCashAmount 原值)
// interest × qty 得到“每 10 份派现额 × 持仓份”,需再 ÷10 才是实际现金;
// 既不能套债券的 /100,也不能直接返回 paymentAmount(那样会放大 10 倍)。
var actualAmount = useBondPriceScale
? BondPriceConverter.ToStorage(paymentAmount)
: paymentAmount;
: paymentAmount / 10;
return actualAmount * longRatio * payDirection;
}
}