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。 // Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值, // 最后的 /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 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; result.Add(new BondPayment { underlyingCode = dividend.UnderlyingCode, payment_date_pl = dividend.EffectiveDate, payment_date = dividend.EffectiveDate, payment_interest = paymentInterest, paying_price = paymentInterest, create_time = dividend.OptDate, update_time = dividend.OptDate, IsCorporateActionCashDividend = true }); } 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) { var payments = GetBondPayments(underlyingCode, startDate, endDate); return CalcPayment(payments, qty, longRatio, payDirection); } /// /// 计算某标的期间现金流。债券期间付息与 Stock/Fund 公司行为现金分红可能同时命中, /// 必须按每条记录的来源单位分别计算,不能按标的类型对整个集合统一除以 10 或 100。 /// /// 期间付息集合 /// 持仓数量 /// 多空方向 /// 收支方向 /// public decimal CalcPayment( List payments, decimal qty, decimal longRatio, decimal payDirection) { 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; } } /// /// /// 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; } } }