using BaseOUDAL; using DocumentFormat.OpenXml.Bibliography; using ExcelDataReader.Log; using YLErp.DBModels; using YLErp.Helpers; namespace YLErp.Modules.EodModule { /// /// 债券期间付息服务 /// public class BondPaymentService : YLBaseService { private static IYcLogger Log = LogFactory.GetLogger(nameof(BondPaymentService)); public BondPaymentService(OptUserInfo userInfo) : base(userInfo) { } public SearchListResult SearchList(BondPaymentReq req) { var valueDtStart = req.ValueDateStart.Year > 2000 ? req.ValueDateStart : DateTime.Today.AddYears(-1); var valueDtEnd = req.ValueDateEnd.Year > 2000 ? req.ValueDateEnd.AddDays(1) : DateTime.Today.AddYears(1); var predicatUn = PredicateBuilder.Create(d => d.LaunchState == "1"); var predicatEoc = PredicateBuilder.Create(source => source.payment_date >= valueDtStart && source.payment_date < valueDtEnd); if (!string.IsNullOrEmpty(req.DataSource)) { predicatEoc = predicatEoc.And(d => d.channel_source.Contains(req.DataSource)); } if (!string.IsNullOrEmpty(req.MarketName)) { predicatUn = predicatUn.And(d => d.MarketName == req.MarketName); } if (!string.IsNullOrEmpty(req.UnderlyingCode)) { predicatEoc = predicatEoc.And(d => d.underlyingCode.Contains(req.UnderlyingCode)); } if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "payment_date"; req.sord = "desc"; } var queryUn = DbContext.underlying_manager.Where(predicatUn).Select(n => new { n.id, n.MarketName, n.UnderlyingCode, n.UnderlyingName, n.UnderlyingInstrumentType, n.InnerCode }); var query = from un in queryUn join source in DbContext.bondPayment.Where(predicatEoc) on un.UnderlyingCode equals source.underlyingCode select new BondPaymentDto { id = source.id, channel_source = source.channel_source, MarketName = un.MarketName, security_id = un.UnderlyingCode, symbol = un.UnderlyingName, coupon_rate = source.coupon_rate, payment_date = source.payment_date, payment_interest = source.payment_interest, payment_parvalue = source.payment_parvalue, create_time = source.create_time, update_time = source.update_time }; var result = query.ToSearchList(req); return result; } public BondPayment SaveBondPayment(BondPayment req) { if (req is null) { throw new ArgumentNullException(nameof(req)); } BondPayment dbmodel; if (req.id == 0) { DbContext.bondPayment.Add(dbmodel = req); } else { dbmodel = DbContext.bondPayment.Find(req.id); if (dbmodel == null) { throw new ServiceException("数据不存在"); } UpdateChanges(dbmodel, req); } dbmodel.update_time = DateTime.Now; DbContext.SaveChanges(); return dbmodel; } /// /// 获取某债券的期间付息情况集合 /// /// /// /// /// public List GetBondPayments(string underlyingCode, DateTime startDate, DateTime endDate) { // GLMS-20260105-0006:票息归属按债权登记日(reg_date)判定,而非支付日(pay_date_PL/pay_date_act)。 // 登记日当天 EOD 即应计提;原按支付日口径会让"登记日≠支付日"的债券漏计(二者恰差一工作日时缺陷被掩盖)。 var result = QueryBondPayments(underlyingCode) .Where(x => x.reg_date > startDate && x.reg_date <= endDate) .AsNoTracking().ToList(); Log.Info($"[分红-登记日口径] GetBondPayments underlyingCode={underlyingCode} 区间=({startDate:yyyy-MM-dd},{endDate:yyyy-MM-dd}] 按reg_date过滤, 命中 {result.Count} 条: " + string.Join(",", result.Select(r => r.reg_date?.ToString("yyyy-MM-dd")))); // 让 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; } /// /// 可测性 seam:返回某债券的全部付息记录(未做日期过滤)。测试可 override 注入内存数据, /// 以验证日期口径(GLMS-20260105-0006:应按债权登记日 reg_date 而非支付日 pay_date_PL/pay_date_act 判定)。 /// protected virtual IQueryable QueryBondPayments(string underlyingCode) => DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode); public List GetTargetDatePayments(string underlyingCode, DateTime targetDate) { var startDate = targetDate.Date; var endDate = startDate.AddDays(1); return DbContext.bondPayment.AsNoTracking().Where(x => x.underlyingCode == underlyingCode && x.payment_date >= startDate && x.payment_date < endDate).ToList(); } /// /// 计算某债券某段时间的期间付息 /// /// 债券代码 /// 计息开始日 /// 计息结束日 /// 持仓数量 /// 多空方向 /// 收支方向 /// public decimal CalcPayment( string underlyingCode, DateTime startDate, DateTime endDate, decimal qty, decimal longRatio, decimal payDirection, bool useBondPriceScale = true) { var payments = GetBondPayments(underlyingCode, startDate, endDate); return CalcPayment(payments, qty, longRatio, payDirection, useBondPriceScale); } /// /// 计算某标的期间现金流。债券与 Stock/Fund 公司行为共用 bond_payment_info, /// 但通过 useBondPriceScale 明确区分两种入库金额单位。 /// /// 期间付息集合 /// 持仓数量 /// 多空方向 /// 收支方向 /// /// 是否按债券报价的百分比口径换算。债券的 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) { 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; return actualAmount * longRatio * payDirection; } } /// /// /// public class BondPaymentReq : BaseSearchReq { /// /// 数据来源 /// public string DataSource { get; set; } /// /// 标的代码 /// public string UnderlyingCode { get; set; } public DateTime ValueDateStart { get; set; } public DateTime ValueDateEnd { get; set; } // 市场 public string MarketName { get; set; } } public class BondPaymentDto : BondPayment { public string MarketName { get; set; } } }