Files
zszq-trs/YLErpDAL/Modules/EodModule/BondPaymentService.cs
T
张名锐 02da9d78a3 refactor(EodModule): 移除债券支付服务中对公司行为同步镜像的依赖
- 不再依赖 bond-sync 镜像,Stock/Fund 公司行为直接作为展示行返回
- 展示金额按"每 10 份派现金额"换算为 GiveCashAmount / 10
- 添加公司行为除权表数据查询逻辑,与现有查询结果合并
- 简化债券支付服务中公司行为处理逻辑,移除去重机制
- 更新现金分红处理逻辑,统一通过 CalcBondPayment 读取 ex_dividend_info
- 移除不再需要的 channel_source 和 jsid 字段设置
2026-08-21 13:50:31 +08:00

275 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BaseOUDAL;
using DocumentFormat.OpenXml.Bibliography;
using ExcelDataReader.Log;
using YLErp.DBModels;
using YLErp.Helpers;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 债券期间付息服务
/// </summary>
public class BondPaymentService : YLBaseService
{
private static IYcLogger Log = LogFactory.GetLogger(nameof(BondPaymentService));
public BondPaymentService(OptUserInfo userInfo) : base(userInfo)
{
}
public SearchListResult<BondPaymentDto> 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<underlying_manager>(d => d.LaunchState == "1");
var predicatEoc = PredicateBuilder.Create<BondPayment>(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
};
// 不再依赖 bond-sync 镜像:Stock/Fund 公司行为直接作为展示行返回。
// 展示金额按“每 10 份派现金额”换算为 GiveCashAmount / 10EOD 计算仍使用
// GetBondPayments 的内部单位口径,不受此处展示换算影响。
var corporateQuery = from un in queryUn
join dividend in DbContext.ex_dividend_info.AsNoTracking()
on un.UnderlyingCode equals dividend.UnderlyingCode
where dividend.ValidStatus
&& dividend.EffectiveDate.HasValue
&& dividend.EffectiveDate.Value >= valueDtStart
&& dividend.EffectiveDate.Value < valueDtEnd
&& dividend.GiveCashAmount != 0
&& (string.IsNullOrEmpty(req.UnderlyingCode)
|| dividend.UnderlyingCode.Contains(req.UnderlyingCode))
&& (un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Stock
|| un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
&& (string.IsNullOrEmpty(req.DataSource)
|| "公司行为除权表".Contains(req.DataSource))
select new BondPaymentDto
{
id = -dividend.id,
channel_source = "公司行为除权表",
MarketName = un.MarketName,
security_id = un.UnderlyingCode,
symbol = un.UnderlyingName,
coupon_rate = null,
payment_date = dividend.EffectiveDate,
payment_interest = dividend.GiveCashAmount / 10m,
payment_parvalue = null,
paying_price = dividend.GiveCashAmount / 10m,
create_time = dividend.OptDate,
update_time = dividend.OptDate
};
// EF Core 无法翻译两个对 BondPaymentDto 继承属性赋值集合不完全一致的投影
// 直接 Concat;分别执行后在内存合并,不改变两组查询的筛选口径。
var rows = query.ToList();
rows.AddRange(corporateQuery.ToList());
var result = rows.AsQueryable().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;
}
/// <summary>
/// 获取某债券的期间付息情况集合
/// </summary>
/// <param name="underylingCode"></param>
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <returns></returns>
public List<BondPayment> 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 由 CalcPayment 非债券分支完成。
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
});
}
return result;
}
/// <summary>
/// 可测性 seam:返回某债券的全部付息记录(未做日期过滤)。测试可 override 注入内存数据,
/// 以验证日期口径(GLMS-20260105-0006:应按债权登记日 reg_date 而非支付日 pay_date_PL/pay_date_act 判定)。
/// </summary>
protected virtual IQueryable<BondPayment> QueryBondPayments(string underlyingCode)
=> DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode);
public List<BondPayment> 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();
}
/// <summary>
/// 计算某债券某段时间的期间付息
/// </summary>
/// <param name="underylingCode">债券代码</param>
/// <param name="startDate">计息开始日</param>
/// <param name="endDate">计息结束日</param>
/// <param name="qty">持仓数量</param>
/// <param name="longRatio">多空方向</param>
/// <param name="payDirection">收支方向</param>
/// <returns></returns>
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);
}
/// <summary>
/// 计算某标的期间现金流。债券与 Stock/Fund 公司行为共用 bond_payment_info
/// 但通过 useBondPriceScale 明确区分两种入库金额单位。
/// </summary>
/// <param name="payments">期间付息集合</param>
/// <param name="qty">持仓数量</param>
/// <param name="longRatio">多空方向</param>
/// <param name="payDirection">收支方向</param>
/// <param name="useBondPriceScale">
/// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息,
/// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红
/// 在 bond_payment_info 中按“每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金,
/// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
/// </param>
/// <returns></returns>
public decimal CalcPayment(
List<BondPayment> 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;
}
}
/// <summary>
///
/// </summary>
public class BondPaymentReq : BaseSearchReq
{
/// <summary>
/// 数据来源
/// </summary>
public string DataSource { get; set; }
/// <summary>
/// 标的代码
/// </summary>
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; }
}
}