Merge branch 'glms/feature/0812_zmr_divPower' into 'glms/feature/1.4.2'
refactor(swap): 统一债券价格处理逻辑 See merge request otc-dev/zszq-trs!13
This commit is contained in:
@@ -439,7 +439,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 获取公司行为公式使用的收盘价。
|
||||
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
|
||||
/// ExDividendDate;不能在 8 月 17 日 EOD 误取 8 月 17 日收盘价重算 8 月 14 日
|
||||
/// ExDividendDate;不能在 除权日 EOD 误取 除权日收盘价重算 登记日
|
||||
/// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。
|
||||
/// </summary>
|
||||
protected virtual decimal GetFundCorporateActionClosePrice(
|
||||
@@ -516,18 +516,22 @@ namespace YLErp.Modules.SwapModule
|
||||
// 公司行为只取 settleDate 当天的有效单行;同一标的出现多条记录必须中止本次收盘,
|
||||
// 否则 ToDictionary 会抛重复键,无法证明哪一条系数应生效。
|
||||
var corporateActionInfos = FindCorporateActionInfos(settleDate) ?? new List<ex_dividend_info>();
|
||||
// 除权日信息
|
||||
var exDividendInfos = corporateActionInfos
|
||||
.Where(x => x != null
|
||||
&& x.ValidStatus
|
||||
&& x.EffectiveDate.HasValue
|
||||
&& x.EffectiveDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
// 登记日信息
|
||||
var registrationInfos = corporateActionInfos
|
||||
.Where(x => x != null
|
||||
&& x.ValidStatus
|
||||
&& x.ExDividendDate.HasValue
|
||||
&& x.ExDividendDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
|
||||
// 公司行为去重 - 除权日
|
||||
var duplicateDividend = exDividendInfos
|
||||
.GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(x => x.Count() > 1);
|
||||
@@ -535,7 +539,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new InvalidOperationException($"标的【{duplicateDividend.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效除权记录");
|
||||
}
|
||||
// 公司行为去重 - 拦截
|
||||
|
||||
// 公司行为去重 - 登记日
|
||||
var duplicateRegistration = registrationInfos
|
||||
.GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(x => x.Count() > 1);
|
||||
@@ -545,6 +550,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 有多条有效记录时,系统无法证明应采用哪一条派现金额,必须中止收盘。
|
||||
throw new InvalidOperationException($"标的【{duplicateRegistration.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效登记日记录");
|
||||
}
|
||||
|
||||
// 根据标的代码 创建map
|
||||
var exDividendByCode = exDividendInfos.ToDictionary(
|
||||
x => x.UnderlyingCode,
|
||||
x => x,
|
||||
@@ -588,21 +595,21 @@ namespace YLErp.Modules.SwapModule
|
||||
var flowEvents = FindFlowEvents(td.id, settleDate);
|
||||
var preDealDate = GetPreDealDate(td.id, settleDate, eventTyps);//上一次平仓/互换/自动互换处理日期
|
||||
List<swap_flow_event> autoInterests = new List<swap_flow_event>();//自动互换利息腿信息
|
||||
|
||||
// 处理浮动腿前先准备当日开盘基线:登记日 EOD 仍保存
|
||||
// 1000 份/100 元,除权日收盘时先把上一 EOD 的基线转换为
|
||||
// 2000 份/50 元,再处理当日平仓 300 份,最终才会得到 1700 份/50 元。
|
||||
// 不能等 DealFloatPositions 处理完平仓后再把 700 份乘 2,否则会错误得到
|
||||
// 1400 份;也不能直接修改数据库里的上一 EOD,否则登记日报表会被污染。
|
||||
|
||||
// 重置基线
|
||||
// 不能等 DealFloatPositions 处理完平仓后再把 700 份乘 2,
|
||||
// 否则会错误得到 1400 份;也不能直接修改数据库里的上一 EOD,否则登记日报表会被污染。
|
||||
// 重置基线 - 除权日
|
||||
var openingEodPositions = PrepareFundOpeningEodPositions(
|
||||
eodPositions,
|
||||
eodPositions, // 上一日终持仓
|
||||
exDividendByCode,
|
||||
settleDate);
|
||||
|
||||
// 构建公司行为前eod持仓
|
||||
var corporateActionBeforePositions = BuildCorporateActionBeforePositions(
|
||||
eodPositions,
|
||||
eodPositions, // 上一日终持仓
|
||||
posiList);
|
||||
|
||||
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
|
||||
@@ -613,18 +620,19 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 处理浮动腿归档
|
||||
var curEodPosis = DealFloatPositions(
|
||||
floatPositionsForCompose,
|
||||
realPosiList,
|
||||
openingEodPositions,
|
||||
todyEodPositions,
|
||||
settleDate,
|
||||
td,
|
||||
preSettleDate,
|
||||
flowEvents);
|
||||
floatPositionsForCompose, // 初始腿
|
||||
realPosiList, // 实时腿
|
||||
openingEodPositions, // 开盘基线
|
||||
todyEodPositions, // 当日终持仓
|
||||
settleDate, // 收盘日期
|
||||
td, // 交易
|
||||
preSettleDate, // 上一交易日
|
||||
flowEvents); // 流水事件
|
||||
|
||||
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
|
||||
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
|
||||
// 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。
|
||||
// 公司行为事件
|
||||
RecordCorporateActionEvents(
|
||||
td,
|
||||
curEodPosis,
|
||||
@@ -632,8 +640,9 @@ namespace YLErp.Modules.SwapModule
|
||||
registrationInfos,
|
||||
exDividendInfos,
|
||||
settleDate);
|
||||
// 登记日 EOD 仍保存除权前快照,但下一交易日开盘读取的实时浮动腿需要
|
||||
// 先切换到生效后的 Q/P。该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||||
// 登记日 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);
|
||||
@@ -668,8 +677,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
|
||||
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 8 月 14 日 EOD 仍是 1000/100;
|
||||
/// 8 月 17 日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 登记日 EOD 仍是 1000/100;
|
||||
/// 除权日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
/// </summary>
|
||||
protected List<eod_swap_position> PrepareFundOpeningEodPositions(
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
@@ -729,6 +738,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var dividendTaxRate = 0m;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
// 不是浮动腿 或者 不是 Fund Stock类型的标的 或者 没有除权信息 或者 除权日不是结算日 - 跳过
|
||||
if (position.PosiDirection <= 0
|
||||
|| !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType)
|
||||
|| string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||||
@@ -739,7 +749,7 @@ namespace YLErp.Modules.SwapModule
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取除权参考价
|
||||
// 获取除权参考价 - 登记日收盘价
|
||||
var corporateActionClosePrice = GetFundCorporateActionClosePrice(
|
||||
dividendInfo,
|
||||
position.UnderlyingPrice);
|
||||
@@ -773,8 +783,11 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
|
||||
// 多空方向
|
||||
var shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||||
// 收付方向
|
||||
var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
// 处理价格的正负号(收支方向)
|
||||
position.PosiNotionalValue = Math.Round(
|
||||
position.PosiGrossPrice * position.PosiQuantity * position.ContractSize,
|
||||
ConsGlobal.MoneyRound,
|
||||
@@ -900,8 +913,9 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日收盘后即切换实时 BOD。EffectiveDate 只用于确认这条记录仍是未来生效的
|
||||
// 公司行为;无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
// 登记日收盘后即切换实时 BOD。
|
||||
// EffectiveDate 只用于确认这条记录仍是未来生效的公司行为;
|
||||
// 无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
var pendingInfos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date)
|
||||
.ToList();
|
||||
@@ -912,6 +926,7 @@ namespace YLErp.Modules.SwapModule
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)
|
||||
&& !string.IsNullOrWhiteSpace(x.UnderlyingCode)))
|
||||
{
|
||||
// 实时腿
|
||||
var realtime = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == td.id
|
||||
&& !x.Invalid
|
||||
&& !x.IsInitial
|
||||
@@ -920,7 +935,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 对每条当日 EOD 浮动腿,按标的代码在 pendingInfos 中找匹配的公司行为。
|
||||
var pending = pendingInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (pending != null)
|
||||
@@ -966,6 +982,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日信息合并除权日信息
|
||||
var infos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Concat(effectiveInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x != null && x.ValidStatus && !string.IsNullOrWhiteSpace(x.UnderlyingCode))
|
||||
@@ -983,6 +1000,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 跟据交易id查当前交易关联事件
|
||||
var existingEvents = FindCorporateActionEvents(td.id);
|
||||
foreach (var current in currentPositions.Where(x => x != null && x.PosiDirection > 0
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)))
|
||||
@@ -1004,16 +1022,18 @@ namespace YLErp.Modules.SwapModule
|
||||
&& x.Data.ExDividendInfoId == info.id
|
||||
&& x.Data.PositionId == current.PositionId)
|
||||
.ToList();
|
||||
// 寻找applied = false的(登记日记录的)
|
||||
var eventData = matchingEvents.FirstOrDefault(x => !x.Data.Applied)
|
||||
?? matchingEvents.FirstOrDefault();
|
||||
var previous = previousPositions?.FirstOrDefault(x => x != null && x.PositionId == current.PositionId);
|
||||
var previous = previousPositions?.FirstOrDefault(x => x != null
|
||||
&& x.PositionId == current.PositionId);
|
||||
// 登记日 false 除权日 true
|
||||
var isEffective = info.EffectiveDate.HasValue
|
||||
&& info.EffectiveDate.Value.Date <= settleDate.Date
|
||||
&& effectiveInfos != null
|
||||
&& effectiveInfos.Any(x => x.id == info.id);
|
||||
|
||||
// 如果没有匹配到事件或事件未生效,则创建新事件。
|
||||
// 如果没有匹配到事件或今天不是除权日 但找到的事件的applied=true(异常事件/重收盘),则创建新事件。
|
||||
if (eventData == null || (!isEffective && eventData.Data.Applied))
|
||||
{
|
||||
// 创建新事件
|
||||
@@ -1184,6 +1204,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal dividendTaxRate,
|
||||
int grossPriceRound)
|
||||
{
|
||||
// 计算除权系数 - adjustCashDividendPrice = false (现金分红模式)
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
closePrice,
|
||||
@@ -3634,9 +3655,10 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
item.FloatRateAbs = item.position.PosiNotionalValue == 0 ? 0 : item.InterestAmount / item.position.PosiNotionalValue;
|
||||
}
|
||||
// 交易录入的债券类收益互换价格以小数保存,展示时转为百分比价格;
|
||||
// 普通收益互换录入的是数量/原始数值,不做乘 100 转换。
|
||||
SetPosiPrice(item.position, item.StructureType == "普通债券类收益互换");
|
||||
// 是否 ×100 由标的资产类型决定(债券价格以小数保存,展示时转为百分比价格),
|
||||
// 与存储层 GetStorageDeliveryPriceRound / GetSwapValuationPrice 的 IsBond 口径一致,
|
||||
// 不依赖簿记结构类型 StructureType。
|
||||
SetPosiPrice(item.position);
|
||||
}
|
||||
return retListResult;
|
||||
}
|
||||
@@ -3681,10 +3703,10 @@ namespace YLErp.Modules.SwapModule
|
||||
position.SwapPositionValue = -position.SwapPositionValue;
|
||||
position.PosiDividendSum = -position.PosiDividendSum;
|
||||
}
|
||||
private void SetPosiPrice(eod_swap_position position, bool? useBondPriceScale = null)
|
||||
private void SetPosiPrice(eod_swap_position position)
|
||||
{
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
|
||||
if (useBondPriceScale ?? (um != null && um.IsBond()))
|
||||
if (um != null && um.IsBond())
|
||||
{
|
||||
position.PosiNetPrice *= 100;
|
||||
position.UnderlyingPrice *= 100;
|
||||
|
||||
@@ -247,8 +247,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为说明使用稳定的键值格式,完整保留调整前后名义本金、价格、数量、
|
||||
/// 待实现分红和现金流变化,操作历史无需重新计算即可核对。
|
||||
/// 公司行为说明仅展示调整前后的名义本金、期初标的价格和持仓数量,
|
||||
/// 便于操作历史直接比对持仓基线。
|
||||
/// </summary>
|
||||
public static string BuildCorporateActionEventReason(CorporateActionEventData data)
|
||||
{
|
||||
@@ -257,37 +257,11 @@ namespace YLErp.Modules.SwapModule
|
||||
return "公司行为快照为空";
|
||||
}
|
||||
|
||||
// 使用 InvariantCulture 固定小数与日期格式,说明文本不随服务器区域设置变化。
|
||||
// 使用 InvariantCulture 固定小数格式,说明文本不随服务器区域设置变化。
|
||||
string D(decimal value) => value.ToString(CultureInfo.InvariantCulture);
|
||||
string Date(DateTime? value) => value.HasValue
|
||||
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||
: "";
|
||||
|
||||
return string.Join("; ", new[]
|
||||
{
|
||||
$"公司行为[{data.UnderlyingCode}]",
|
||||
$"ExDividendDate={Date(data.ExDividendDate)}",
|
||||
$"EffectiveDate={Date(data.EffectiveDate)}",
|
||||
$"ExDividendInfoId={data.ExDividendInfoId}",
|
||||
$"PositionId={data.PositionId}",
|
||||
$"GiveCashAmount={D(data.GiveCashAmount)}",
|
||||
$"GiveShareAmount={D(data.GiveShareAmount)}",
|
||||
$"Split={(data.Split.HasValue ? D(data.Split.Value) : "")}",
|
||||
$"RationedSharesAmount={D(data.RationedSharesAmount)}",
|
||||
$"RationedSharesPrice={D(data.RationedSharesPrice)}",
|
||||
"调整前",
|
||||
$"BeforeNotional={D(data.BeforeNotional)}",
|
||||
$"BeforePrice={D(data.BeforePrice)}",
|
||||
$"BeforeQuantity={D(data.BeforeQuantity)}",
|
||||
$"BeforePendingDividend={D(data.BeforePendingDividend)}",
|
||||
"调整后",
|
||||
$"AfterNotional={D(data.AfterNotional)}",
|
||||
$"AfterPrice={D(data.AfterPrice)}",
|
||||
$"AfterQuantity={D(data.AfterQuantity)}",
|
||||
$"AfterPendingDividend={D(data.AfterPendingDividend)}",
|
||||
$"CashFlowChange={D(data.CashFlowChange)}",
|
||||
$"Applied={data.Applied}"
|
||||
});
|
||||
return $"调整前:名义本金:{D(data.BeforeNotional)} 期初标的价格:{D(data.BeforePrice)} 持仓数量:{D(data.BeforeQuantity)}"
|
||||
+ Environment.NewLine
|
||||
+ $"调整后:名义本金:{D(data.AfterNotional)} 期初标的价格:{D(data.AfterPrice)} 持仓数量:{D(data.AfterQuantity)}";
|
||||
}
|
||||
|
||||
public void DeleteEvent(int tradeId)
|
||||
|
||||
Reference in New Issue
Block a user