Merge remote-tracking branch 'dest/glms/feature/1.4.2' into test
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule;
|
||||
|
||||
/// <summary>
|
||||
/// 构造普通收益互换结算单单行数据所需的业务输入。
|
||||
/// 数据均来自平仓事件及其关联交易,避免依赖平仓后可能已不存在的日终持仓。
|
||||
/// 数据来自平仓事件、关联交易及结算日日终持仓。
|
||||
/// </summary>
|
||||
public sealed class SwapSettlementBillRowInput
|
||||
{
|
||||
@@ -40,6 +40,9 @@ public sealed class SwapSettlementBillRowInput
|
||||
/// <summary>平仓事件浮动腿记录的期末结算收益率(展示态数值)。</summary>
|
||||
public decimal? ExitYtm { get; set; }
|
||||
|
||||
/// <summary>结算日日终持仓中的当日浮动端分红,保留原始收付方向。</summary>
|
||||
public decimal CurrentDayFloatingDividend { get; set; }
|
||||
|
||||
/// <summary>期间付息或分红是否计入本次净额结算。</summary>
|
||||
public bool IncludePeriodPaymentInNetting { get; set; }
|
||||
}
|
||||
@@ -61,7 +64,6 @@ public static class SwapSettlementBillRowBuilder
|
||||
var positions = input.Positions ?? Array.Empty<swap_position>();
|
||||
var eventFlows = input.EventFlows ?? Array.Empty<swap_flow_event>();
|
||||
|
||||
// 三个业务日期是结算单和文件命名的必填项,缺失时直接阻止生成不完整附件。
|
||||
var startDate = input.Trade.StartDate
|
||||
?? throw new ServiceException("结算单缺少交易起始日");
|
||||
var eventDate = input.CloseFlow.UnwindDate
|
||||
@@ -69,9 +71,8 @@ public static class SwapSettlementBillRowBuilder
|
||||
var payDate = input.CloseFlow.PayDate
|
||||
?? throw new ServiceException("结算单缺少支付日");
|
||||
|
||||
// 将同一事件的普通利息与预付金利息分开;预付金本金仅统计结算日前已生效的腿。
|
||||
var floatingPosition = positions.FirstOrDefault(x => x.PositionType > 0)
|
||||
?? positions.FirstOrDefault(x => !ConsTrade.InterestMarginModels.Contains(x.InterestMode));
|
||||
|
||||
var settlementPosition = positions.FirstOrDefault(x => x.id == input.CloseFlow.PositionId);
|
||||
var interestEvents = eventFlows
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode))
|
||||
.ToList();
|
||||
@@ -88,25 +89,25 @@ public static class SwapSettlementBillRowBuilder
|
||||
var floatingAmount = -input.CloseFlow.MarkClosePnl;
|
||||
var fee = -(input.CloseFlow.TradingFee + input.CloseFlow.TradingFeePending);
|
||||
var marginInterest = -marginEvents.Sum(x => x.InterestClosePnL);
|
||||
var periodAmount = -input.CloseFlow.DividendIn;
|
||||
var initialMargin = SumMargin(effectiveMargins, InterestModeEnum.初始预付金);
|
||||
var additionalMargin = SumMargin(effectiveMargins, InterestModeEnum.追加预付金);
|
||||
|
||||
var additionalMarginPositions = positions
|
||||
.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金)
|
||||
.ToList();
|
||||
var additionalMargin = SumMargin(additionalMarginPositions, InterestModeEnum.追加预付金);
|
||||
|
||||
// 净额结算按实际轧差项求和;到期结算在净额基础上返还或收取期初、追加预付金。
|
||||
var netSettlementAmount = interestAmount + floatingAmount + fee + marginInterest
|
||||
+ (input.IncludePeriodPaymentInNetting ? periodAmount : 0m);
|
||||
|
||||
var netSettlementAmount = -eventFlows.Sum(x => x.InterestClosePnL)
|
||||
- input.CloseFlow.FloatPnlSum;
|
||||
var maturitySettlementAmount = netSettlementAmount + initialMargin + additionalMargin;
|
||||
var floatRateAbs = input.CloseNotionalValue == 0m
|
||||
? 0m
|
||||
: interestAmount / input.CloseNotionalValue;
|
||||
|
||||
// 同一数据库字段在模板中按标的类型拆分:现券展示期间付息,ETF 展示期间分红。
|
||||
|
||||
var isCashBond = ConsGlobal.InstrumentType.IsBond(input.UnderlyingInstrumentType);
|
||||
var isEtf = ConsGlobal.InstrumentType.Fund.Equals(
|
||||
input.UnderlyingInstrumentType,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 此处集中完成模板字段映射和展示精度处理,生成器只负责组装原始业务数据。
|
||||
|
||||
return new ExcelReportModel
|
||||
{
|
||||
TradeNumber = input.ConfirmNo,
|
||||
@@ -119,17 +120,21 @@ public static class SwapSettlementBillRowBuilder
|
||||
InterestRate = interestEvents.Sum(x => x.InterestRate).ToString("0.00%"),
|
||||
PosiNotionalValue = input.CloseNotionalValue.ToString("0.00"),
|
||||
Quantity = input.CloseFlow.Quantity.ToString("0.00"),
|
||||
DividendIn = isCashBond ? periodAmount.ToString("0.00") : string.Empty,
|
||||
PeriodDividend = isEtf ? periodAmount.ToString("0.00") : string.Empty,
|
||||
PosiNetPrice = ((floatingPosition?.PosiGrossPrice ?? 0m) * 100m).ToString("0.00000000"),
|
||||
DividendIn = isCashBond
|
||||
? input.CurrentDayFloatingDividend.ToString("0.00")
|
||||
: string.Empty,
|
||||
PeriodDividend = isCashBond
|
||||
? string.Empty
|
||||
: input.CurrentDayFloatingDividend.ToString("0.00"),
|
||||
PosiNetPrice = ((settlementPosition?.PosiGrossPrice ?? 0m) * 100m).ToString("0.00000000"),
|
||||
InitYtm = isCashBond && input.Trade.InitYtm.HasValue
|
||||
? input.Trade.InitYtm.Value.ToString("0.####%")
|
||||
: string.Empty,
|
||||
ClosePrice = (input.CloseFlow.TradingAmountAvg * 100m).ToString("0.00000000"),
|
||||
ExitYtm = isCashBond && input.ExitYtm.HasValue
|
||||
ExitYtm = input.ExitYtm.HasValue
|
||||
? input.ExitYtm.Value.ToString("0.0000")
|
||||
: string.Empty,
|
||||
RateDays = Math.Max(0, (eventDate - startDate).Days + 1).ToString(),
|
||||
RateDays = Math.Max(0, (eventDate - startDate).Days).ToString(),
|
||||
FloatRateAbs = floatRateAbs.ToString("0.0000%"),
|
||||
FloatRate = floatRateAbs.ToString("0.0000%"),
|
||||
InterestAmount = interestAmount.ToString("0.00"),
|
||||
@@ -139,7 +144,9 @@ public static class SwapSettlementBillRowBuilder
|
||||
.ToString("0.0000%"),
|
||||
MarginInterestAmount = marginInterest.ToString("0.00"),
|
||||
InitialMargin = initialMargin.ToString("0.00"),
|
||||
AdditionalMargin = additionalMargin.ToString("0.00"),
|
||||
AdditionalMargin = additionalMarginPositions.Count > 0
|
||||
? additionalMargin.ToString("0.00")
|
||||
: string.Empty,
|
||||
MarginAmout = Math.Abs(initialMargin).ToString("0.00"),
|
||||
MarkClosePnl = (-input.CloseFlow.FloatPnlSum).ToString("0.00"),
|
||||
NetSettleAmout = netSettlementAmount.ToString("0.00"),
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace YLErp.BLL
|
||||
/// </summary>
|
||||
public const string LackOfMoney = "LackOfMoney";
|
||||
|
||||
/// <summary>
|
||||
/// 保存交易时预付金授信不足:UI 确认后按 剩余授信+现金差额 拆分预付金腿再保存(§2.3 保存前拆单)
|
||||
/// </summary>
|
||||
public const string MarginCreditSplit = "MarginCreditSplit";
|
||||
|
||||
public const string RiskWarningConfirm = "RiskWarningConfirm";
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user