feat(swap): 添加基金现金分红处理功能

- 实现 ApplyFundCashDividends 方法处理基金现金分红逻辑
- 现金分红在生效日 EOD 即执行,计入已实现分红字段
- 从盯市盈亏中分离分红金额,避免收益重复计算
- 更新测试用例验证现金分红正确记入已实现分红
- 修正测试方法名称反映实际功能变更
- 完善单元测试验证分红金额计算准确性
This commit is contained in:
张名锐
2026-08-18 15:43:11 +08:00
parent aa1a13d7a3
commit ddf233678d
2 changed files with 90 additions and 10 deletions
@@ -107,6 +107,11 @@ namespace YLErp.Modules.SwapModule
flowEvents,
dividendInfos.ToDictionary(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase),
SettleDate);
ApplyFundCashDividends(
positions,
previousEodPositions,
dividendInfos.ToDictionary(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase),
SettleDate);
}
}
@@ -327,7 +332,7 @@ namespace YLErp.Modules.SwapModule
}
[TestMethod]
public void SPC_FUND_002_现金分红_只调整价格不生成分红流水()
public void SPC_FUND_002_现金分红_收盘时记入已实现分红()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
@@ -342,22 +347,20 @@ namespace YLErp.Modules.SwapModule
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(CreateFundCorporateAction(cashAmount: 10m));
var actual = previousEod.Clone();
actual.ValueDate = SettleDate;
actual.UnderlyingPrice = 100m;
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate);
service.ExecuteFundCorporateActions(
new[] { actual },
new[] { previousEod },
Array.Empty<swap_flow_event>(),
service.ExDividendInfos);
var actual = service.CreatedEodPositions.Single(x => x.PositionId == 1);
Assert.AreEqual(1000m, actual.PosiQuantity);
Assert.AreEqual(0m, actual.TdChangedQty);
Assert.AreEqual(99m, actual.PosiGrossPrice);
Assert.AreEqual(0m, actual.TdPosiDividend);
Assert.AreEqual(1000m, actual.TdPosiDividend);
Assert.AreEqual(0m, actual.PosiDividendSum);
Assert.AreEqual(99000m, actual.PosiNotionalValue);
Assert.AreEqual(0m, actual.PosiMtmPnL);
Assert.AreEqual(0m, actual.PosiProfitSum);
Assert.AreEqual(1000m, actual.RealizedDividend);
Assert.AreEqual(1000m, actual.RealizedPnl);
}
[TestMethod]
@@ -591,6 +591,15 @@ namespace YLErp.Modules.SwapModule
td,
preSettleDate,
flowEvents);
// 公司行为 - 分红
// Fund 现金分红在 EffectiveDate 当日收盘即完成结算:
// TdPosiDividend 展示当日金额,RealizedDividend 累计已实现金额,
// 不把同一笔金额留在 PosiDividendSum 待实现字段中。
ApplyFundCashDividends(
curEodPosis,
eodPositions,
exDividendByCode,
settleDate);
var posiLongNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
var posiShortNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue);
var closePosiNotional = curEodPosis.Where(s => s.TdCloseQty > 0).Sum(s => s.TdCloseQty * s.ContractSize * s.PosiGrossPrice);
@@ -805,6 +814,74 @@ namespace YLErp.Modules.SwapModule
}
}
/// <summary>
/// 将 Fund 当日现金分红记入 EOD 已实现分红。
/// GiveCashAmount 按每 10 份金额计算:1000 份、每 10 份派 10,结果为 1000。
/// 现金分红在生效日 EOD 即执行,因此 PosiDividendSum 不增加本次金额,
/// 同时从除权价格变化产生的 PosiMtmPnL 中剥离,避免收益重复计算。
/// </summary>
protected void ApplyFundCashDividends(
IReadOnlyCollection<eod_swap_position> currentEodPositions,
IReadOnlyCollection<eod_swap_position> previousEodPositions,
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
DateTime settleDate)
{
if (currentEodPositions == null
|| previousEodPositions == null
|| exDividendByCode == null
|| exDividendByCode.Count == 0)
{
return;
}
var dividendTaxRate = GetDividendTaxRate();
foreach (var current in currentEodPositions)
{
if (current == null
|| current.PosiDirection == 0
|| current.UnderlyingInstrumentType != ConsGlobal.InstrumentType.Fund
|| string.IsNullOrWhiteSpace(current.UnderlyingCode)
|| !exDividendByCode.TryGetValue(current.UnderlyingCode, out var dividendInfo)
|| !dividendInfo.EffectiveDate.HasValue
|| dividendInfo.EffectiveDate.Value.Date != settleDate.Date)
{
continue;
}
var previous = previousEodPositions.FirstOrDefault(
x => x != null && x.PositionId == current.PositionId);
var entitlementQuantity = previous?.PosiQuantity ?? current.PosiQuantity;
var directionRatio = DirectionRatio.ReceivePay(current.PosiDirection);
var currentDividend = entitlementQuantity > 0m
? entitlementQuantity / 10m
* dividendInfo.GiveCashAmount
* (1m - dividendTaxRate)
* directionRatio
: 0m;
// 当日浮动端分红
current.TdPosiDividend = RoundMoney(currentDividend);
var previousDividendSum = previous?.PosiDividendSum ?? 0m;
// 浮动端平仓盈亏·分红未实现 = 未实现分红总和 - 当日浮动端平仓盈亏·分红
current.PosiDividendSum = current.PosiQuantity > 0m
? RoundMoney(previousDividendSum - current.TdCloseDividend)
: 0m;
// 浮动端平仓盈亏·盯市未实现 = 盯市未实现 - 当日浮动端分红
// current.PosiMtmPnL = RoundMoney(current.PosiMtmPnL - current.TdPosiDividend);
// 浮动端已实现·分红 = 已实现分红 + 当日浮动端分红
current.RealizedDividend = RoundMoney(current.RealizedDividend + current.TdPosiDividend);
// 浮动端已实现·盈亏 = 盈亏 + 当日浮动端分红
current.PosiProfitSum = RoundMoney(MtmCalc.ReturnLegProfitSum(
current.PosiMtmPnL,
current.PosiDividendSum,
current.PosiFeePending));
SetFloatingRealizedPnl(current);
current.SwapPositionValue = RoundMoney(PositionValueCalc.Calc(
current.InterestProfitSum,
current.PosiProfitSum));
}
}
/// <summary>
/// 将一条真实生效日公司行为应用到盘中实时 Fund 浮动腿。
/// 盘中先复制严格早于 valueDate 的 EOD,再调用此方法;因此重复调用时每次都会