feat(swap): 添加基金现金分红处理功能
- 实现 ApplyFundCashDividends 方法处理基金现金分红逻辑 - 现金分红在生效日 EOD 即执行,计入已实现分红字段 - 从盯市盈亏中分离分红金额,避免收益重复计算 - 更新测试用例验证现金分红正确记入已实现分红 - 修正测试方法名称反映实际功能变更 - 完善单元测试验证分红金额计算准确性
This commit is contained in:
@@ -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,再调用此方法;因此重复调用时每次都会
|
||||
|
||||
Reference in New Issue
Block a user