- 在 BondPayment 模型中添加 IsCorporateActionCashDividend 属性用于区分不同数据来源 - 修改 BondPaymentService 中的 CalcPayment 方法,按每条记录来源分别处理金额单位转换 - 更新单元测试以验证混合债券付息和公司行为分红的正确计算 - 移除不再需要的 useBondPriceScale 参数简化方法接口 - 修正注释说明以反映新的按记录类型分别计算的逻辑
268 lines
13 KiB
C#
268 lines
13 KiB
C#
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 / 10;EOD 计算仍使用
|
||
// 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; }
|
||
}
|
||
}
|