Files
zszq-trs/YLErpDAL/Modules/EodModule/BondPaymentService.cs
T
张名锐 392e36a820 feat(bond): 添加公司行为现金分红标识并优化付息计算逻辑
- 在 BondPayment 模型中添加 IsCorporateActionCashDividend 属性用于区分不同数据来源
- 修改 BondPaymentService 中的 CalcPayment 方法,按每条记录来源分别处理金额单位转换
- 更新单元测试以验证混合债券付息和公司行为分红的正确计算
- 移除不再需要的 useBondPriceScale 参数简化方法接口
- 修正注释说明以反映新的按记录类型分别计算的逻辑
2026-08-21 16:51:55 +08:00

268 lines
13 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 只应用于这类内存补充记录;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;
}
/// <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)
{
var payments = GetBondPayments(underlyingCode, startDate, endDate);
return CalcPayment(payments, qty, longRatio, payDirection);
}
/// <summary>
/// 计算某标的期间现金流。债券期间付息与 Stock/Fund 公司行为现金分红可能同时命中,
/// 必须按每条记录的来源单位分别计算,不能按标的类型对整个集合统一除以 10 或 100。
/// </summary>
/// <param name="payments">期间付息集合</param>
/// <param name="qty">持仓数量</param>
/// <param name="longRatio">多空方向</param>
/// <param name="payDirection">收支方向</param>
/// <returns></returns>
public decimal CalcPayment(
List<BondPayment> payments,
decimal qty,
decimal longRatio,
decimal payDirection)
{
var actualAmount = (payments ?? new List<BondPayment>()).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;
}
}
/// <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; }
}
}