feat(swap): 添加公司行为现金分红按登记日权益数量计算功能

- 新增 CalcBondPayment 方法支持传入公司行为权益数量参数
- 在 SwapEodPositionService 中识别现金分红类型的公司行为持仓
- 修改 CopyEodPosition 和 UpdateEodPosition 方法传递公司行为前的数量信息
- 更新 SaveCurrentEodInitalPosi 方法支持公司行为数量计算
- 在 BondPaymentService 中实现公司行为现金分红按登记日权益数量计算逻辑
- 添加单元测试验证公司行为现金分红使用除权前数量的计算功能
This commit is contained in:
张名锐
2026-08-26 10:33:51 +08:00
parent 2b8c310b89
commit 6c582ac958
3 changed files with 69 additions and 12 deletions
@@ -66,5 +66,23 @@ namespace YLErp.Modules.EodModule
Assert.AreEqual(204000m, actual);
}
[TestMethod]
public void CalcPayment_CorporateActionUsesPreCorporateActionQuantity()
{
var payments = new List<BondPayment>
{
new BondPayment { payment_interest = 2m },
new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true }
};
var method = typeof(BondPaymentService).GetMethod(
nameof(BondPaymentService.CalcPayment),
new[] { typeof(List<BondPayment>), typeof(decimal), typeof(decimal), typeof(decimal), typeof(decimal?) });
Assert.IsNotNull(method, "公司行为现金分红需要支持单独传入除权前数量。");
var actual = (decimal)method.Invoke(CreateService(), new object[] { payments, 2000m, 1m, 1m, (decimal?)1000m });
Assert.AreEqual(1040m, actual, "原生付息按当前 2000 份计算为 40,公司行为分红按除权前 1000 份计算为 1000。");
}
}
}
@@ -189,10 +189,26 @@ namespace YLErp.Modules.EodModule
decimal qty,
decimal longRatio,
decimal payDirection)
{
return CalcPayment(payments, qty, longRatio, payDirection, null);
}
/// <summary>
/// 公司行为现金分红按登记日权益数量计算;同一窗口中的原生债券付息仍按当前持仓数量计算。
/// </summary>
public decimal CalcPayment(
List<BondPayment> payments,
decimal qty,
decimal longRatio,
decimal payDirection,
decimal? corporateActionQty)
{
var actualAmount = (payments ?? new List<BondPayment>()).Sum(payment =>
{
var paymentAmount = (payment.payment_interest ?? 0m) * qty;
var paymentQty = payment.IsCorporateActionCashDividend
? corporateActionQty ?? qty
: qty;
var paymentAmount = (payment.payment_interest ?? 0m) * paymentQty;
// bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的
// 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。
return payment.IsCorporateActionCashDividend
@@ -357,6 +357,19 @@ namespace YLErp.Modules.SwapModule
directionRatio);
}
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate,
decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty)
{
if (!corporateActionQty.HasValue)
{
return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
}
var service = new BondPaymentService(UserInfo);
var payments = service.GetBondPayments(underlyingCode, fromDate, toDate);
return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty);
}
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
@@ -611,6 +624,11 @@ namespace YLErp.Modules.SwapModule
var corporateActionBeforePositions = BuildCorporateActionBeforePositions(
eodPositions, // 上一日终持仓
posiList);
var corporateActionCashDividendBeforePositions = corporateActionBeforePositions
.Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode)
&& exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend)
&& dividend.GiveCashAmount != 0m)
.ToList();
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
@@ -627,7 +645,8 @@ namespace YLErp.Modules.SwapModule
settleDate, // 收盘日期
td, // 交易
preSettleDate, // 上一交易日
flowEvents); // 流水事件
flowEvents, // 流水事件
corporateActionCashDividendBeforePositions);
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
@@ -1435,7 +1454,8 @@ namespace YLErp.Modules.SwapModule
DateTime settleDate,
trade td,
DateTime preSettleDate,
List<swap_flow_event> flowEvents)
List<swap_flow_event> flowEvents,
IReadOnlyCollection<eod_swap_position> corporateActionBeforePositions = null)
{
string settleDateStr = settleDate.ToString("yyyy-MM-dd");
string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd");
@@ -1457,18 +1477,20 @@ namespace YLErp.Modules.SwapModule
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息
var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息
var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id);
var corporateActionBeforeQuantity = corporateActionBeforePositions?
.FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity;
eod_swap_position eodPosi = new eod_swap_position();
if (eodPosition == null)
{
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents);
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
}
else if (unwindEvents.Count() == 0)
{
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate);
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity);
}
else
{
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents);
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
}
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
list.Add(eodPosi);
@@ -2626,7 +2648,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="todayPositions">当日日终归档信息</param>
/// <param name="swap_Deals">当日平仓/互换事件信息</param>
/// <param name="td">交易信息</param>
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, decimal? corporateActionBeforeQuantity = null)
{
if (curretEod == null)
{
@@ -2648,7 +2670,7 @@ namespace YLErp.Modules.SwapModule
decimal tax = um.ValueAddedTax ?? 0;
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
@@ -2710,7 +2732,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="curretEod"></param>
/// <param name="td"></param>
/// <param name="valueDate"></param>
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
{
if (curretEod == null)
{
@@ -2744,7 +2766,7 @@ namespace YLErp.Modules.SwapModule
// 修改,互换事件会影响待实现的分红的,现在要算上
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
@@ -2871,7 +2893,8 @@ namespace YLErp.Modules.SwapModule
/// <param name="position"></param>
/// <param name="td"></param>
/// <param name="settleDate"></param>
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate,
DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
{
eod_swap_position curretEod = new eod_swap_position();
var um = GetUnderlyingData(position.UnderlyingCode);
@@ -2924,7 +2947,7 @@ namespace YLErp.Modules.SwapModule
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{
decimal tax = um.ValueAddedTax ?? 0;
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio);
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
payment = DividendCalc.AfterTax(payment, tax);
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
curretEod.TdPosiDividend = payment;