Merge branch 'glms/feature/0812_zmr_divPower' into 'glms/feature/1.4.2'

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

See merge request otc-dev/zszq-trs!16
This commit is contained in:
张名锐
2026-08-26 02:35:03 +00:00
3 changed files with 69 additions and 12 deletions
@@ -66,5 +66,23 @@ namespace YLErp.Modules.EodModule
Assert.AreEqual(204000m, actual); 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 qty,
decimal longRatio, decimal longRatio,
decimal payDirection) 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 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 补充的 // bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的
// 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。 // 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。
return payment.IsCorporateActionCashDividend return payment.IsCorporateActionCashDividend
@@ -357,6 +357,19 @@ namespace YLErp.Modules.SwapModule
directionRatio); 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 分支)---- // ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary> /// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
@@ -611,6 +624,11 @@ namespace YLErp.Modules.SwapModule
var corporateActionBeforePositions = BuildCorporateActionBeforePositions( var corporateActionBeforePositions = BuildCorporateActionBeforePositions(
eodPositions, // 上一日终持仓 eodPositions, // 上一日终持仓
posiList); posiList);
var corporateActionCashDividendBeforePositions = corporateActionBeforePositions
.Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode)
&& exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend)
&& dividend.GiveCashAmount != 0m)
.ToList();
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。 // 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。 // 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
@@ -627,7 +645,8 @@ namespace YLErp.Modules.SwapModule
settleDate, // 收盘日期 settleDate, // 收盘日期
td, // 交易 td, // 交易
preSettleDate, // 上一交易日 preSettleDate, // 上一交易日
flowEvents); // 流水事件 flowEvents, // 流水事件
corporateActionCashDividendBeforePositions);
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment // 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。 // 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
@@ -1435,7 +1454,8 @@ namespace YLErp.Modules.SwapModule
DateTime settleDate, DateTime settleDate,
trade td, trade td,
DateTime preSettleDate, 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 settleDateStr = settleDate.ToString("yyyy-MM-dd");
string preSettleDateStr = preSettleDate.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 tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息
var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息 var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息
var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id); 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(); eod_swap_position eodPosi = new eod_swap_position();
if (eodPosition == null) if (eodPosition == null)
{ {
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents); eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
} }
else if (unwindEvents.Count() == 0) else if (unwindEvents.Count() == 0)
{ {
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate); eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity);
} }
else 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)}"); Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
list.Add(eodPosi); list.Add(eodPosi);
@@ -2655,7 +2677,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="todayPositions">当日日终归档信息</param> /// <param name="todayPositions">当日日终归档信息</param>
/// <param name="swap_Deals">当日平仓/互换事件信息</param> /// <param name="swap_Deals">当日平仓/互换事件信息</param>
/// <param name="td">交易信息</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) if (curretEod == null)
{ {
@@ -2677,7 +2699,7 @@ namespace YLErp.Modules.SwapModule
decimal tax = um.ValueAddedTax ?? 0; decimal tax = um.ValueAddedTax ?? 0;
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 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.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
} }
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0; curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
@@ -2739,7 +2761,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="curretEod"></param> /// <param name="curretEod"></param>
/// <param name="td"></param> /// <param name="td"></param>
/// <param name="valueDate"></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) if (curretEod == null)
{ {
@@ -2773,7 +2795,7 @@ namespace YLErp.Modules.SwapModule
// 修改,互换事件会影响待实现的分红的,现在要算上 // 修改,互换事件会影响待实现的分红的,现在要算上
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 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.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
} }
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
@@ -2900,7 +2922,8 @@ namespace YLErp.Modules.SwapModule
/// <param name="position"></param> /// <param name="position"></param>
/// <param name="td"></param> /// <param name="td"></param>
/// <param name="settleDate"></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(); eod_swap_position curretEod = new eod_swap_position();
var um = GetUnderlyingData(position.UnderlyingCode); var um = GetUnderlyingData(position.UnderlyingCode);
@@ -2953,7 +2976,7 @@ namespace YLErp.Modules.SwapModule
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0) if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{ {
decimal tax = um.ValueAddedTax ?? 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); payment = DividendCalc.AfterTax(payment, tax);
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红 //var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
curretEod.TdPosiDividend = payment; curretEod.TdPosiDividend = payment;