Merge remote-tracking branch 'dest/glms/feature/1.4.2' into test

This commit is contained in:
lisong
2026-08-27 15:20:46 +08:00
86 changed files with 2863 additions and 599 deletions
@@ -773,9 +773,9 @@ namespace YLErp.Modules.TradeModule.DealModule
bool adjustCashDividendPrice = true)
{
// 价格调整模式除权参考价 =
// 收盘价 * 10 - 【每股派息 * 10 * (1-分红税率)】 + 配股数 * 配股价
// - -----------------------------------------------------
// (10 + 送股数 + 配股数) * 拆股倍数
// 登记日收盘价 * 10 - 【每股派息 * 10 * (1-分红税率)】 + 配股数 * 配股价
// ---------------------------------------------------------------
// (10 + 送股数 + 配股数) * 拆股倍数
// 场内链路默认继续把现金派息计入除权参考价;
// TRS Stock/Fund 现金模式显式关闭该项 :“【】” 号内数据。
var cashPriceAdjustment = adjustCashDividendPrice
@@ -784,8 +784,8 @@ namespace YLErp.Modules.TradeModule.DealModule
// 拆股倍数
var splitFactor = GetSplitFactor(info);
// 除权参考价(TRS
// 收盘价 * 10 + 配股数 * 配股价
// ------------------------------
// 登记日收盘价 * 10 + 配股数 * 配股价
// -------------------------------------
// (10 + 送股数 + 配股数) * 拆股倍数
var exDividendPrice = ((closePrice * 10m - cashPriceAdjustment
+ info.RationedSharesAmount * info.RationedSharesPrice)
@@ -985,7 +985,7 @@ namespace YLErp.Modules.TradeModule.DealModule
&& (excludedId <= 0 || O.id != excludedId));
}
private static void MergeNonZeroDividendValues(ex_dividend_info target, ex_dividend_info source)
private static void MergeDividendValues(ex_dividend_info target, ex_dividend_info source)
{
if (target == null)
{
@@ -996,26 +996,11 @@ namespace YLErp.Modules.TradeModule.DealModule
throw new ArgumentNullException(nameof(source));
}
// 同一业务键可能分别来自多行导入,或来自“数据库旧记录 + 当前导入记录”
// 每个字段独立合并:当前值非零时覆盖旧值,当前值为零时保留旧值,
// 这样派息、送股、配股数量、配股价格可以从不同来源补齐到同一行。
// 该约定将零解释为“未提供”,因此不能通过普通导入把已有字段显式清零。
if (source.GiveCashAmount != 0m)
{
target.GiveCashAmount = source.GiveCashAmount;
}
if (source.GiveShareAmount != 0m)
{
target.GiveShareAmount = source.GiveShareAmount;
}
if (source.RationedSharesAmount != 0m)
{
target.RationedSharesAmount = source.RationedSharesAmount;
}
if (source.RationedSharesPrice != 0m)
{
target.RationedSharesPrice = source.RationedSharesPrice;
}
// 数值字段(包括 0)都是有效的覆盖值
target.GiveCashAmount = source.GiveCashAmount;
target.GiveShareAmount = source.GiveShareAmount;
target.RationedSharesAmount = source.RationedSharesAmount;
target.RationedSharesPrice = source.RationedSharesPrice;
if (source.Split.HasValue)
{
// Split 为空表示本次未提供,不能按历史兼容值 1 清空或覆盖旧倍数;明确提供 1 才覆盖。
@@ -1098,7 +1083,7 @@ namespace YLErp.Modules.TradeModule.DealModule
}
// 先在当前批次内按业务键归并。第一条记录作为待保存目标,后续记录
// 只补充/覆盖非零字段,不会因为重复行而生成多条数据库记录。
// 后续同一业务键记录会覆盖字段,不会因生成多条数据库记录。
if (preparedIndexes.TryGetValue(businessKey, out var preparedIndex))
{
var preparedItem = preparedInfos[preparedIndex].Item;
@@ -1111,7 +1096,7 @@ namespace YLErp.Modules.TradeModule.DealModule
return false;
}
MergeNonZeroDividendValues(preparedItem, item);
MergeDividendValues(preparedItem, item);
if (item.id > 0)
{
recordKeys[item.id] = businessKey;
@@ -1159,9 +1144,10 @@ namespace YLErp.Modules.TradeModule.DealModule
}
else
{
if (checkDividendInfoExecuteStatus(dividend))
var executingTradeNumber = GetDividendInfoExecutingTradeNumber(dividend);
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
{
errMsg = $"{dividend.UnderlyingCode} {dividend.ExDividendDate?.ToString("yyyy-MM-dd")}除权信息保存失败,该信息已被执行,不允许修改!";
errMsg = $"不可修改,有交易【{executingTradeNumber}】使用了该条除权除息数据";
return false;
}
var conflictingDividend = FindExDividendByBusinessKey(underlying.id, itemDate, dividend.id);
@@ -1174,10 +1160,7 @@ namespace YLErp.Modules.TradeModule.DealModule
dividend.UnderlyingCode = item.UnderlyingCode;
dividend.UnderlyingId = item.UnderlyingId;
dividend.ExDividendDate = item.ExDividendDate;
// 数据库已有记录也必须走与批次内重复行相同的合并规则:导入字段非零
// 才覆盖旧值,导入字段为零则保留数据库存量值,避免一次不完整导入
// 把旧的派息/送股/配股信息误清零。
MergeNonZeroDividendValues(dividend, item);
MergeDividendValues(dividend, item);
dividend.ValidStatus = true;
dividend.DataSource = ExDividendDataSources.Manual;
dividend.SourceUpdatedAt = sourceUpdatedAt;
@@ -1223,6 +1206,14 @@ namespace YLErp.Modules.TradeModule.DealModule
/// <param name="info"></param>
/// <returns></returns>
public bool checkDividendInfoExecuteStatus(ex_dividend_info info)
{
return !string.IsNullOrWhiteSpace(GetDividendInfoExecutingTradeNumber(info));
}
/// <summary>
/// 返回仍在引用已执行公司行为的交易编号;无引用时返回空字符串。
/// </summary>
public string GetDividendInfoExecutingTradeNumber(ex_dividend_info info)
{
// TRS 公司行为以 EffectiveDate 为真正生效边界。登记日创建待生效事件不应锁定
// 维护;只有交易已经完成 EffectiveDate(例如收盘到 7 月 30 日,而真实除权日为
@@ -1230,34 +1221,37 @@ namespace YLErp.Modules.TradeModule.DealModule
if (info?.EffectiveDate.HasValue == true)
{
var effectiveDate = info.EffectiveDate.Value.Date;
var trsTradeIds = DbContext.trade
var trsTrades = DbContext.trade
.Where(x => x.ValidState != ConsGlobal.InValid
&& x.TradeType == "收益互换"
&& x.UnderlyingCode == info.UnderlyingCode
&& x.TradeDate <= effectiveDate
&& x.ExerciseDate >= effectiveDate)
.Select(x => x.id)
.Select(x => new { x.id, x.TradeNumber })
.ToList();
if (trsTradeIds.Count > 0)
if (trsTrades.Count > 0)
{
// 是否仍被交易引用以当前有效 EOD 为准。公司行为事件本身是不可篡改
// 历史,交易回退后仍会保留;若仅凭 Applied 事件锁定,回退到登记日前
// 也无法纠错。生效日及以后还有有效 EOD 才表示当前仍已执行。
var hasAppliedEod = DbContext.eod_swap_position.Any(x =>
var trsTradeIds = trsTrades.Select(x => x.id).ToList();
var appliedTradeId = DbContext.eod_swap_position.Where(x =>
trsTradeIds.Contains(x.SwapTradeId)
&& !x.Invalid
&& x.UnderlyingCode == info.UnderlyingCode
&& x.ValueDate >= effectiveDate);
if (hasAppliedEod)
&& x.ValueDate >= effectiveDate)
.Select(x => x.SwapTradeId)
.FirstOrDefault();
if (appliedTradeId > 0)
{
return true;
return trsTrades.First(x => x.id == appliedTradeId).TradeNumber;
}
// EffectiveDate 已存在时,当前有效 EOD 是唯一执行状态来源。
// 回退会清理生效日及之后的 EOD,但不会删除 eodStatus 或不可篡改的
// 公司行为审计事件;此处不能继续落入旧的登记日 eodStatus 判断,
// 否则交易已回退仍会被错误判定为“已执行”而无法修改。
return false;
return string.Empty;
}
}
@@ -1268,20 +1262,22 @@ namespace YLErp.Modules.TradeModule.DealModule
var tradeQuery = from t in DbContext.trade.Where(O => O.UnderlyingCode == info.UnderlyingCode && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate)
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
select et.id;
if (tradeQuery.Any())
select t.TradeNumber;
var executingTradeNumber = tradeQuery.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
{
return true;
return executingTradeNumber;
}
//查询篮子标的对应交易是否执行过收盘操作;
var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.CommodityCode == "篮子标的" && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray();
tradeQuery = from t in DbContext.trade.Where(O => umList.Contains(O.UnderlyingCode) && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate)
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
select et.id;
if (tradeQuery.Any())
select t.TradeNumber;
executingTradeNumber = tradeQuery.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
{
return true;
return executingTradeNumber;
}
//查询多标的对应交易是否执行过收盘操作;
tradeQuery = from ts in DbContext.trade_swap_detail.Where(O => O.UnderlyingCode == info.UnderlyingCode)
@@ -1289,13 +1285,14 @@ namespace YLErp.Modules.TradeModule.DealModule
on ts.TradeId equals t.id
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
select et.id;
if (tradeQuery.Any())
select t.TradeNumber;
executingTradeNumber = tradeQuery.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
{
return true;
return executingTradeNumber;
}
}
return false;
return string.Empty;
}
public List<DividendTrade> QueryDividendTrade(DividendTradeReq req)
@@ -380,10 +380,11 @@ namespace YLErp.Modules.TradeModule.DealModule
}
var cashService = new ClientCashInCashOutService(this);
cashService.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate,0);
// R4 授信/现金标签:预付金腿定稿资金标签(选授信按剩余授信分配,不足跨界腿拆单),
// R4 授信/现金标签:预付金腿定稿资金标签(按授信分配的腿——腿选授信或未选回退交易级资金来源——
// 按剩余授信分配,不足跨界腿拆单为 原腿授信+新现金腿),
// 授信部分不产生资金流水(只写授信出入表占用),现金部分产生 应付预付金 记录;特批全现金。
// 必须在 InitialPosition/AddPositionEvent 之前执行:实时持仓克隆与初始事件要继承"定稿后"的标签,
// 拆单新拆出的授信腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。
// 拆单新拆出的现金腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。
var generateMarginLegs = new List<swap_position>();
foreach (var marginPosition in td.swap_positions)
{