feat(bond): 支持股票和基金现金分红纳入债券支付计算 - 收紧过度开发行为
- 实现股票/基金现金分红数据从 ex_dividend_info 同步到 BondPayment - 新增公司行为去重机制,避免镜像任务完成后重复计息 - 统一现金分红存储口径为"每 10 份派现金额",保持与同步任务一致性 - 修改 CalcPayment 方法,股票/基金分红需除以 10 转换实际现金金额 - 添加单元测试验证债券票息和股票/基金分红的不同计算方式 - 更新文档注释说明"每 10 份派现金额"存储规范 - 修复公司行为生效日处理逻辑,确保正确应用除权系数 - 扩展测试覆盖股票类证券的公司行为处理场景
This commit is contained in:
@@ -78,7 +78,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从已经确认的 Fund EOD 快照恢复实时浮动腿的有效基线。
|
||||
/// 从已经确认的 Stock/Fund EOD 快照恢复实时浮动腿的有效基线。
|
||||
/// 该方法只复制 EOD 已落库的数量、价格、名义本金及累计分红/待结费用,不再次计算
|
||||
/// 公司行动系数,因此是幂等的。例:原 1000 份、期初价 100,10 送 10 后 EOD 为
|
||||
/// 2000 份、50;下一日盘中直接恢复 2000/50,不能再变成 4000/25。
|
||||
@@ -112,7 +112,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 valueDate 之前最近一份有效 Fund EOD 快照,作为盘中操作的日初基线。
|
||||
/// 查询 valueDate 之前最近一份有效 Stock/Fund EOD 快照,作为盘中操作的日初基线。
|
||||
/// 必须严格使用 < valueDate:试算日当天的 EOD 可能尚未完成,或是重收盘留下的待重建数据,
|
||||
/// 不能反向覆盖盘中实时持仓。例:D 日 10 送 10 后 EOD 为 2000 份/50,D+1 盘中读取 D;
|
||||
/// D 日盘中只读取 D-1,不会误把 D 日半成品当成已生效基线。Invalid 明细始终排除。
|
||||
@@ -446,25 +446,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找结算日有效的公司行为记录;测试可替换为内存数据。
|
||||
/// settleDate 必须是收盘作业使用的日期边界(通常为 00:00:00),这里沿用完整
|
||||
/// DateTime 相等匹配;历史数据若带时分秒或为空,不会被静默归入当天,需在作业前
|
||||
/// 通过数据预检查处理,而不是让收盘在错误基线上继续计算。
|
||||
/// </summary>
|
||||
protected virtual List<ex_dividend_info> FindExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return DbContext.ex_dividend_info
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.EffectiveDate.HasValue
|
||||
&& x.EffectiveDate.Value == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询登记日或真实生效日命中的公司行为。保留 FindExDividendInfos 这个
|
||||
/// 可替换入口,测试和历史调用方可以继续注入内存数据。
|
||||
/// </summary>
|
||||
/// <summary>查询登记日或真实生效日命中的有效公司行为。</summary>
|
||||
protected virtual List<ex_dividend_info> FindCorporateActionInfos(DateTime settleDate)
|
||||
{
|
||||
return DbContext.ex_dividend_info
|
||||
@@ -490,19 +472,6 @@ namespace YLErp.Modules.SwapModule
|
||||
UpdateDbOption(swapEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找登记日公司行为。登记日只创建待生效审计事件,不参与当日持仓系数计算;
|
||||
/// EffectiveDate 到达后才由 FindExDividendInfos 命中并改变 Stock/Fund 基线。
|
||||
/// </summary>
|
||||
protected virtual List<ex_dividend_info> FindRegistrationExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return FindCorporateActionInfos(settleDate)
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.ExDividendDate.HasValue
|
||||
&& x.ExDividendDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公司行为公式使用的收盘价。
|
||||
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
|
||||
@@ -523,16 +492,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return Convert.ToDecimal(closePrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为现金分红使用的系统税率,小数形式。
|
||||
/// 系统配置按百分数存储(例如 13 表示 13%),公司行为公式需要 0.13;送股本身
|
||||
/// 不受该税率影响;此处只负责读取并换算,不在异常时擅自默认为 0。
|
||||
/// </summary>
|
||||
protected virtual decimal GetDividendTaxRate()
|
||||
{
|
||||
return new DividendService(this).GetDividendTaxRateDecimal();
|
||||
}
|
||||
|
||||
public static bool IsCorporateActionInstrument(string instrumentType)
|
||||
{
|
||||
// TRS 公司行为本期只覆盖 Stock/Fund。TBonds 等类型继续走原债券付息链路,
|
||||
@@ -700,7 +659,7 @@ namespace YLErp.Modules.SwapModule
|
||||
flowEvents);
|
||||
|
||||
// 现金分红不在登记日直接读取 ex_dividend_info 累加。
|
||||
// 同步任务会把 GiveCashAmount/10 写入 bond_payment_info,Copy/Update EOD 在
|
||||
// 同步任务会把 GiveCashAmount(每 10 份派现金额)写入 bond_payment_info,Copy/Update EOD 在
|
||||
// EffectiveDate 通过 CalcBondPayment 命中该行并生成 TdPosiDividend。
|
||||
// 这样登记日快照不提前变化,也不会与债券付息/平仓链路重复计算。
|
||||
RecordCorporateActionEvents(
|
||||
@@ -710,6 +669,9 @@ namespace YLErp.Modules.SwapModule
|
||||
registrationInfos,
|
||||
exDividendInfos,
|
||||
settleDate);
|
||||
// 登记日 EOD 仍保存除权前快照,但下一交易日开盘读取的实时浮动腿需要
|
||||
// 先切换到生效后的 Q/P。该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||||
UpdateRealtimeCorporateActionPositions(td, curEodPosis, registrationInfos, exDividendInfos, 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);
|
||||
@@ -741,7 +703,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Fund 公司行为。
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
|
||||
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 8 月 14 日 EOD 仍是 1000/100;
|
||||
/// 8 月 17 日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
@@ -765,10 +727,8 @@ namespace YLErp.Modules.SwapModule
|
||||
.Select(x => x.Clone())
|
||||
.ToList();
|
||||
// 应用公司行为
|
||||
ApplyFundCorporateActions(
|
||||
ApplyCorporateActions(
|
||||
openingPositions,
|
||||
Array.Empty<eod_swap_position>(),
|
||||
Array.Empty<swap_flow_event>(),
|
||||
exDividendByCode,
|
||||
settleDate);
|
||||
return openingPositions;
|
||||
@@ -791,10 +751,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// EffectiveDate 再生成开盘基线。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
protected void ApplyFundCorporateActions(
|
||||
protected void ApplyCorporateActions(
|
||||
IEnumerable<eod_swap_position> positions,
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
IReadOnlyCollection<swap_flow_event> flowEvents,
|
||||
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
|
||||
DateTime settleDate)
|
||||
{
|
||||
@@ -803,8 +761,9 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取系统税率
|
||||
var dividendTaxRate = GetDividendTaxRate();
|
||||
// TODO: 现金分红税率接入后,仅价格调整模式需要读取税率;TRS 现金模式下不参与除权系数。
|
||||
// var dividendTaxRate = GetDividendTaxRate();
|
||||
var dividendTaxRate = 0m;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
if (position.PosiDirection <= 0
|
||||
@@ -824,41 +783,7 @@ namespace YLErp.Modules.SwapModule
|
||||
if (corporateActionClosePrice <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
}
|
||||
|
||||
// 当天已有 EOD 且该腿没有流水时,Copy 分支不会恢复价格字段。
|
||||
// 先还原前一日基线,避免同一结算日重跑时再次除权。
|
||||
var hasPositionFlow = flowEvents.Any(x => x.PositionId == position.PositionId);
|
||||
var previousPosition = previousEodPositions.FirstOrDefault(
|
||||
x => x.PositionId == position.PositionId);
|
||||
// 没有流水 且 有前一日持仓时,恢复前一日价格字段
|
||||
if (!hasPositionFlow && previousPosition != null)
|
||||
{
|
||||
position.PosiQuantity = previousPosition.PosiQuantity;
|
||||
position.PosiGrossPrice = previousPosition.PosiGrossPrice;
|
||||
position.PosiNetPrice = previousPosition.PosiNetPrice;
|
||||
position.PosiNetFeePrice = previousPosition.PosiNetFeePrice;
|
||||
position.PosiNetNoFeePrice = previousPosition.PosiNetNoFeePrice;
|
||||
}
|
||||
|
||||
var denominator = 10m + dividendInfo.GiveShareAmount + dividendInfo.RationedSharesAmount;
|
||||
if (denominator == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】在【{settleDate:yyyy-MM-dd}】的除权份额参数导致除数为 0");
|
||||
}
|
||||
|
||||
// 计算除权系数
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】在【{settleDate:yyyy-MM-dd}】计算得到无效除权系数");
|
||||
$"Stock/Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
}
|
||||
|
||||
// Excel 公式 口径:PriceRatio 是“登记日收盘价 / 除权参考价”,
|
||||
@@ -866,37 +791,24 @@ namespace YLErp.Modules.SwapModule
|
||||
// 配股已经进入 价格参考价,所以即使没有送股,配股也会调整 TRS 数量;
|
||||
// 现金分红不影响 TRS Stock/Fund 期初价格,现金权益由独立分红字段处理。
|
||||
var originalQuantity = position.PosiQuantity;
|
||||
position.PosiQuantity = Math.Round(
|
||||
originalQuantity * factors.PriceRatio,
|
||||
12,
|
||||
MidpointRounding.AwayFromZero);
|
||||
// 计算公司行为发生后的 Q/P
|
||||
var adjusted = CalculateCorporateActionValues(
|
||||
position.PosiQuantity,
|
||||
position.PosiGrossPrice,
|
||||
position.PosiNetPrice,
|
||||
position.PosiNetFeePrice,
|
||||
position.PosiNetNoFeePrice,
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode));
|
||||
position.PosiQuantity = adjusted.Quantity;
|
||||
position.TdChangedQty = position.PosiQuantity - originalQuantity;
|
||||
|
||||
var storagePriceRound = GetStorageDeliveryPriceRound(
|
||||
position.UnderlyingInstrumentType,
|
||||
position.UnderlyingCode);
|
||||
position.PosiGrossPrice = Math.Round(
|
||||
position.PosiGrossPrice / factors.PriceRatio,
|
||||
storagePriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiNetPrice = Math.Round(
|
||||
position.PosiNetPrice / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
if (position.PosiNetFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetFeePrice = Math.Round(
|
||||
position.PosiNetFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (position.PosiNetNoFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetNoFeePrice = Math.Round(
|
||||
position.PosiNetNoFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
position.PosiGrossPrice = adjusted.GrossPrice;
|
||||
position.PosiNetPrice = adjusted.NetPrice;
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
|
||||
var shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||||
var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
@@ -998,16 +910,79 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
var closePrice = GetFundCorporateActionClosePrice(info, position.PosiGrossPrice);
|
||||
ApplyFundCorporateActionToPosition(
|
||||
ApplyCorporateActionToPosition(
|
||||
position,
|
||||
info,
|
||||
closePrice,
|
||||
GetDividendTaxRate());
|
||||
0m);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步公司行为后的实时浮动腿。
|
||||
/// 登记日只更新下一交易日 BOD 使用的实时 Q/P,不改当日已落库的 EOD;
|
||||
/// 生效日则把已调整的 EOD 复制到实时腿。每次都先从当日 EOD 恢复,保证重跑幂等。
|
||||
/// </summary>
|
||||
private void UpdateRealtimeCorporateActionPositions(
|
||||
trade td,
|
||||
IReadOnlyCollection<eod_swap_position> currentEodPositions,
|
||||
IReadOnlyCollection<ex_dividend_info> registrationInfos,
|
||||
IReadOnlyCollection<ex_dividend_info> effectiveInfos,
|
||||
DateTime settleDate)
|
||||
{
|
||||
if (td == null || currentEodPositions == null || currentEodPositions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日收盘后即切换实时 BOD。EffectiveDate 只用于确认这条记录仍是未来生效的
|
||||
// 公司行为;无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
var pendingInfos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date)
|
||||
.ToList();
|
||||
var appliedInfos = effectiveInfos ?? Array.Empty<ex_dividend_info>();
|
||||
|
||||
foreach (var eod in currentEodPositions.Where(x => x != null
|
||||
&& x.PosiDirection > 0
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)
|
||||
&& !string.IsNullOrWhiteSpace(x.UnderlyingCode)))
|
||||
{
|
||||
var realtime = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == td.id
|
||||
&& !x.Invalid
|
||||
&& !x.IsInitial
|
||||
&& x.PositionId == eod.PositionId);
|
||||
if (realtime == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pending = pendingInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (pending != null)
|
||||
{
|
||||
// 必须从登记日 EOD 基线生成下一交易日 BOD,而不是在旧实时腿上继续套系数;
|
||||
// 这样 100000/100 只会变成一次 200000/50,并且不会把初始腿改掉。
|
||||
// 先将实时腿恢复为登记日 EOD 的旧基线,再只对实时腿应用一次公司行为。
|
||||
// EOD 仍保持除权前快照;因此 7/10 EOD=100000/100,而 7/13 BOD=200000/50。
|
||||
var baseline = eod.Clone();
|
||||
UpdateSwapPositionWithRealTime(baseline);
|
||||
var closePrice = GetFundCorporateActionClosePrice(pending, baseline.PosiGrossPrice);
|
||||
ApplyCorporateActionToPosition(realtime, pending, closePrice, 0m);
|
||||
continue;
|
||||
}
|
||||
|
||||
var applied = appliedInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (applied != null)
|
||||
{
|
||||
// 生效日 EOD 已经完成 Q/P 调整,实时腿直接同步最终快照,不再二次套系数。
|
||||
UpdateSwapPositionWithRealTime(eod.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入公司行为生命周期审计事件。
|
||||
/// 登记日:保存调整前快照并标记 Applied=false;
|
||||
@@ -1212,72 +1187,66 @@ namespace YLErp.Modules.SwapModule
|
||||
return SwapEventService.BuildCorporateActionEventReason(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧测试/扩展调用的直接现金分红辅助方法。
|
||||
/// GiveCashAmount 按每 10 份金额计算:1000 份、每 10 份派 10,结果为 1000。
|
||||
/// 当前生产 SwapPositionCompose 不再调用此方法:公司行为现金分红由同步任务
|
||||
/// 写入 bond_payment_info,EffectiveDate 收盘通过 CalcBondPayment 进入 EOD,
|
||||
/// 以避免登记日提前入账及与债券付息链路重复。保留方法是为了不破坏已有测试替身
|
||||
/// 或外部扩展类的编译契约;新增业务代码不得再直接传入 ex_dividend_info。
|
||||
/// </summary>
|
||||
protected void ApplyFundCashDividends(
|
||||
IReadOnlyCollection<eod_swap_position> currentEodPositions,
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
|
||||
DateTime settleDate)
|
||||
/// <summary>公司行为调整后的持仓 Q/P 结果,供 EOD、实时腿和盘中平仓共用。</summary>
|
||||
private readonly struct CorporateActionValues
|
||||
{
|
||||
if (currentEodPositions == null
|
||||
|| exDividendByCode == null
|
||||
|| exDividendByCode.Count == 0)
|
||||
public CorporateActionValues(decimal quantity, decimal grossPrice, decimal netPrice, decimal? netFeePrice, decimal? netNoFeePrice)
|
||||
{
|
||||
return;
|
||||
Quantity = quantity;
|
||||
GrossPrice = grossPrice;
|
||||
NetPrice = netPrice;
|
||||
NetFeePrice = netFeePrice;
|
||||
NetNoFeePrice = netNoFeePrice;
|
||||
}
|
||||
|
||||
var dividendTaxRate = GetDividendTaxRate();
|
||||
var previousList = previousEodPositions ?? Array.Empty<eod_swap_position>();
|
||||
foreach (var current in currentEodPositions)
|
||||
public decimal Quantity { get; }
|
||||
public decimal GrossPrice { get; }
|
||||
public decimal NetPrice { get; }
|
||||
public decimal? NetFeePrice { get; }
|
||||
public decimal? NetNoFeePrice { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一计算公司行为后的 Q/P。EOD、实时腿和盘中平仓只负责提供基线,
|
||||
/// 不再各自复制数量、毛价和净价的调整公式。
|
||||
/// </summary>
|
||||
private static CorporateActionValues CalculateCorporateActionValues(
|
||||
decimal quantity,
|
||||
decimal grossPrice,
|
||||
decimal netPrice,
|
||||
decimal? netFeePrice,
|
||||
decimal? netNoFeePrice,
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal closePrice,
|
||||
decimal dividendTaxRate,
|
||||
int grossPriceRound)
|
||||
{
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
closePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
if (current == null
|
||||
|| current.PosiDirection == 0
|
||||
|| !IsTrsCorporateActionInstrument(current.UnderlyingInstrumentType)
|
||||
|| string.IsNullOrWhiteSpace(current.UnderlyingCode)
|
||||
|| !exDividendByCode.TryGetValue(current.UnderlyingCode, out var dividendInfo)
|
||||
|| !dividendInfo.ExDividendDate.HasValue
|
||||
|| dividendInfo.ExDividendDate.Value.Date != settleDate.Date)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previous = previousList.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;
|
||||
|
||||
// 当日浮动端分红。公司行为现金分红采用现金模式:不调期初价格,
|
||||
// 只增加待实现分红,支付日仍由既有 DealDividends/付息链路结算。
|
||||
current.TdPosiDividend = RoundMoney(currentDividend);
|
||||
var previousDividendSum = previous?.PosiDividendSum ?? 0m;
|
||||
// 浮动端平仓盈亏·分红未实现 = 前日待实现 + 当日公司行为分红
|
||||
// - 当日已实现分红;本次公司行为尚未支付,因此不能写入 RealizedDividend。
|
||||
current.PosiDividendSum = current.PosiQuantity > 0m
|
||||
? RoundMoney(previousDividendSum + current.TdPosiDividend - current.TdCloseDividend)
|
||||
: 0m;
|
||||
// 现金模式不从 PosiMtmPnL 剥离分红:价格没有被除权,分红只存在于待实现字段。
|
||||
current.PosiProfitSum = RoundMoney(MtmCalc.ReturnLegProfitSum(
|
||||
current.PosiMtmPnL,
|
||||
current.PosiDividendSum,
|
||||
current.PosiFeePending));
|
||||
SetFloatingRealizedPnl(current);
|
||||
current.SwapPositionValue = RoundMoney(PositionValueCalc.Calc(
|
||||
current.InterestProfitSum,
|
||||
current.PosiProfitSum));
|
||||
throw new InvalidOperationException(
|
||||
$"标的【{dividendInfo?.UnderlyingCode}】计算得到无效除权系数");
|
||||
}
|
||||
|
||||
var adjustedQuantity = Math.Round(quantity * factors.PriceRatio, 12, MidpointRounding.AwayFromZero);
|
||||
var adjustedGrossPrice = Math.Round(grossPrice / factors.PriceRatio, grossPriceRound, MidpointRounding.AwayFromZero);
|
||||
var adjustedNetPrice = Math.Round(netPrice / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
var adjustedNetFeePrice = netFeePrice.HasValue
|
||||
? Math.Round(netFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||||
: (decimal?)null;
|
||||
var adjustedNetNoFeePrice = netNoFeePrice.HasValue
|
||||
? Math.Round(netNoFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||||
: (decimal?)null;
|
||||
return new CorporateActionValues(
|
||||
adjustedQuantity,
|
||||
adjustedGrossPrice,
|
||||
adjustedNetPrice,
|
||||
adjustedNetFeePrice,
|
||||
adjustedNetNoFeePrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1288,7 +1257,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 现金模式调用公式时使用 adjustCashDividendPrice=false,现金权益只进入分红字段,
|
||||
/// 不改变 Stock/Fund 的期初价格。
|
||||
/// </summary>
|
||||
public static bool ApplyFundCorporateActionToPosition(
|
||||
public static bool ApplyCorporateActionToPosition(
|
||||
swap_position position,
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal corporateActionClosePrice,
|
||||
@@ -1303,44 +1272,21 @@ namespace YLErp.Modules.SwapModule
|
||||
return false;
|
||||
}
|
||||
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
var adjusted = CalculateCorporateActionValues(
|
||||
position.PosiQuantity,
|
||||
position.PosiGrossPrice,
|
||||
position.PosiNetPrice,
|
||||
position.PosiNetFeePrice,
|
||||
position.PosiNetNoFeePrice,
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】计算得到无效除权系数");
|
||||
}
|
||||
|
||||
var originalQuantity = position.PosiQuantity;
|
||||
position.PosiQuantity = Math.Round(
|
||||
originalQuantity * factors.PriceRatio,
|
||||
12,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiGrossPrice = Math.Round(
|
||||
position.PosiGrossPrice / factors.PriceRatio,
|
||||
ConsGlobal.SwapDeliveryPriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiNetPrice = Math.Round(
|
||||
position.PosiNetPrice / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
if (position.PosiNetFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetFeePrice = Math.Round(
|
||||
position.PosiNetFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (position.PosiNetNoFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetNoFeePrice = Math.Round(
|
||||
position.PosiNetNoFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
ConsGlobal.SwapDeliveryPriceRound);
|
||||
position.PosiQuantity = adjusted.Quantity;
|
||||
position.PosiGrossPrice = adjusted.GrossPrice;
|
||||
position.PosiNetPrice = adjusted.NetPrice;
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
position.PosiNotionalValue = Math.Round(
|
||||
position.PosiGrossPrice * position.PosiQuantity * position.ContractSize,
|
||||
ConsGlobal.MoneyRound,
|
||||
|
||||
Reference in New Issue
Block a user