Merge remote-tracking branch 'dest/glms/feature/1.4.2' into test
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
@@ -180,5 +182,103 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的纯展示口径。
|
||||
/// 浮动腿盯市收益、开平仓费用和普通利息分别计算;保证金腿的利息
|
||||
/// 仅作为估值组成项保留一次,不混入新 Tab 的普通利息列。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? ExerciseDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
// 日终明细以 UnderlyingCode 是否存在区分浮动腿和利息腿;调用方即使传入混合集合,
|
||||
// 这里也会重新过滤,避免保证金/利息数据被带入浮动端新口径。
|
||||
var floating = (floatingLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && !string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
var interests = (interestLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
// MarginModes 覆盖初始/维持保证金相关腿。它们的利息不属于需求中的“利息端待实现收益”,
|
||||
// 但必须单独保留,以使两个合约估值与旧口径总额保持一致。
|
||||
var ordinaryInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var marginInterests = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var firstFloating = floating.FirstOrDefault();
|
||||
|
||||
// PosiGrossPrice 已是 EOD 归档口径的期初全价;债券价格不可在报表接口再次乘 100。
|
||||
var initialPrice = firstFloating?.PosiGrossPrice;
|
||||
// PosiFeePending 是日终归一后的我方损益方向:支付费用为负、收取费用为正。
|
||||
// 本列独立展示它,下面的 valuation 再加回一次,不能因展示拆列而改变合约估值。
|
||||
var openingClosingFee = floating.Sum(x => x.PosiFeePending);
|
||||
// PosiMtmPnL 已排除分红和费用,避免从 PosiProfitSum 重复拆分历史费用。
|
||||
var floatingUnrealizedPnl = floating.Sum(x => x.PosiMtmPnL);
|
||||
var ordinaryInterestPnl = ordinaryInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
var marginInterestAmount = marginInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
|
||||
// 新口径估值 = 去费用浮动收益 + 开平仓费用 + 普通利息 + 保证金利息。
|
||||
// “浮动端待实现收益”列不包含费用,而合约估值仍沿用旧总额,故费用只能在此加一次。
|
||||
var valuation = floatingUnrealizedPnl
|
||||
+ openingClosingFee
|
||||
+ ordinaryInterestPnl
|
||||
+ marginInterestAmount;
|
||||
var result = new EodSwapRiskNewFields
|
||||
{
|
||||
UnderlyingInstrumentType = firstFloating?.UnderlyingInstrumentType,
|
||||
UnderlyingDirection = string.Join(",", floating
|
||||
.Select(x => x.PositionType == (int)PositionTypeFlag.Long ? "多头"
|
||||
: x.PositionType == (int)PositionTypeFlag.Short ? "空头" : "")
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
UnderlyingCode = string.Join(",", floating
|
||||
.Select(x => x.UnderlyingCode)
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
InitialPrice = initialPrice,
|
||||
NotionalQuantity = notionalValue,
|
||||
ContractStartDate = startDate,
|
||||
ContractMaturityDate = ExerciseDate,
|
||||
// 只要普通利息腿存在 FR007,即按需求显示 FR007;保证金腿不影响该展示基准。
|
||||
InterestBenchmark = ordinaryInterests.Any(x =>
|
||||
!string.IsNullOrWhiteSpace(x.FloatRateUnderlyingCode)
|
||||
&& x.FloatRateUnderlyingCode.IndexOf("FR007", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
? "FR007" : "固定利率",
|
||||
// 使用日终当日实际适用的 TdInterestRate 合计,而非合同初始利率或利差字段。
|
||||
InterestRatePrice = ordinaryInterests.Sum(x => x.TdInterestRate),
|
||||
OpeningClosingFee = -openingClosingFee,
|
||||
// 合约浮动端待实现收益
|
||||
FloatingUnrealizedPnl = floatingUnrealizedPnl,
|
||||
// 合约利息端待实现收益
|
||||
OrdinaryInterestPnl = ordinaryInterestPnl,
|
||||
MarginInterestAmount = marginInterestAmount,
|
||||
MarginInterestGain = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付)
|
||||
.Sum(x => Math.Abs(x.InterestIncomeSum)),
|
||||
MarginInterestLoss = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取)
|
||||
.Sum(x => -Math.Abs(x.InterestIncomeSum))
|
||||
};
|
||||
|
||||
// DividendPayDate=0 表示到期才与本金轧差,期间付息/分红需要加进该口径;
|
||||
// 其余支付方式则由现金支付承担期间金额,估值字段不再包含 periodAmount。
|
||||
if (dividendPayDate == 0)
|
||||
{
|
||||
result.MaturityNettingValuation = valuation + periodAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.PeriodPaymentValuation = valuation;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,49 @@ public static class FundTagCalc
|
||||
return plans;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存前授信拆单(§2.3 保存前拆单,2026-08-26 业务确认):把确认成交阶段的物理拆分前移到录入保存——
|
||||
/// 对 NeedSplit 的腿:原腿保留授信部分(InterestPrincipalFix 按可用额度折算)标 Credit,
|
||||
/// 克隆一条现金差额腿(倒挤守恒)标 Cash 返回(Obervation 置空,防 SaveSwapPositions 重复插观察配置);
|
||||
/// 不拆的授信偏好腿同步定稿标签:全额授信→Credit、额度为0/耗尽全额现金→Cash;
|
||||
/// 现金/默认腿不动(最终定稿仍由确认成交 ApplyMarginFundTags 兜底重写)。
|
||||
/// legs 与 plans 须为 AllocateByLegPreference 的同序输入输出。占用/流水仍发生在确认成交。
|
||||
/// </summary>
|
||||
public static List<swap_position> ApplySaveTimeSplit(List<LegAmount> legs, List<LegFundPlan> plans)
|
||||
{
|
||||
var newLegs = new List<swap_position>();
|
||||
for (var i = 0; i < plans.Count; i++)
|
||||
{
|
||||
if (!legs[i].PreferCredit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var plan = plans[i];
|
||||
if (plan.NeedSplit)
|
||||
{
|
||||
var position = plan.Leg;
|
||||
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
|
||||
var payableRatio = position.InterestDirection == 1 ? 1 : -1;
|
||||
var originalFix = position.InterestPrincipalFix;
|
||||
position.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.FundTag = ConsFundTag.Credit;
|
||||
var cashLeg = position.Clone();
|
||||
cashLeg.id = 0;
|
||||
cashLeg.PositionId = 0;
|
||||
cashLeg.Obervation = null;
|
||||
//现金腿倒挤 = 原 fix − 授信 fix(分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
cashLeg.InterestPrincipalFix = originalFix - position.InterestPrincipalFix;
|
||||
cashLeg.FundTag = ConsFundTag.Cash;
|
||||
newLegs.Add(cashLeg);
|
||||
}
|
||||
else
|
||||
{
|
||||
plan.Leg.FundTag = plan.CreditAmount > 0 ? ConsFundTag.Credit : ConsFundTag.Cash;
|
||||
}
|
||||
}
|
||||
return newLegs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平仓/到期返还金额按被平仓腿的 FundTag 分流(§2.4):
|
||||
/// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",出金方向记正数),Cash/无标签(存量)走现金。
|
||||
@@ -102,8 +145,8 @@ public class LegFundPlan
|
||||
public double CreditAmount { get; set; }
|
||||
/// <summary>现金部分金额</summary>
|
||||
public double CashAmount { get; set; }
|
||||
/// <summary>拆单时新拆出的授信腿(占用记录绑定到它)</summary>
|
||||
public swap_position CreditLeg { get; set; }
|
||||
/// <summary>拆单时新拆出的现金腿(授信不足的差额;占用记录绑原腿、现金流水绑它)</summary>
|
||||
public swap_position CashLeg { get; set; }
|
||||
public bool NeedSplit => CreditAmount > 0 && CashAmount > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"资金记录。
|
||||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 累计保证金(应付预付金+追加保证金 流水净额 + 追加授信占用);
|
||||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 已缴保证金净额
|
||||
/// (应付预付金现金净收额 + 初始授信占用净额 + 追加保证金现金累计 + 追加授信占用累计——
|
||||
/// 2026-08-27 修正:授信垫付的初始预付金不产生应付预付金流水,此前未计入已缴导致每个结算日按维持全额重复开追加);
|
||||
/// 现金部分为逐结算日增量记录(BUG-03 修正:每结算日一条、Money=−increment,键 TradeId+Action+Deal+HappenDate 幂等),
|
||||
/// 需求上升只增不减;授信优先(阶段二规则):授信部分只写授信出入表(remark 前缀=追加保证金,position_id 空、冗余 trade_id)。
|
||||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算",重跑时 目标/已补足 不变 → 新增为 0 不重复写。
|
||||
@@ -107,6 +109,16 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = g.Sum(x => x.amount) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||||
|
||||
//初始预付金的授信占用净额(非"追加保证金"前缀:簿记初始占用 + 平仓释放取负,Σ(amount) 即净已缴):
|
||||
//授信腿不产生应付预付金流水,目标追加里只扣现金净收额会把授信垫付的初始预付金漏掉——
|
||||
//每个结算日都按维持保证金全额重复开追加(BUG:多收授信占用/现金,2026-08-27 交易2538实证:初始授信200万未扣、首日全额追加280.8万)
|
||||
var initCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
|
||||
.Where(x => x.trade_id != null && tradeIds.Contains(x.trade_id ?? 0)
|
||||
&& (x.remark == null || !x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark)))
|
||||
.GroupBy(x => x.trade_id)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = g.Sum(x => x.amount) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||||
|
||||
var fundTagService = new SwapFundTagService(this);
|
||||
var cashService = new ClientCashInCashOutService(this);
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
@@ -121,8 +133,11 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance,
|
||||
payableNetByTrade.TryGetValue(td.id, out var payableNet) ? payableNet : 0);
|
||||
//目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额,
|
||||
//授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加)
|
||||
var payableNet = (payableNetByTrade.TryGetValue(td.id, out var payable) ? payable : 0)
|
||||
+ (initCreditByTrade.TryGetValue(td.id, out var initCredit) ? initCredit : 0);
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet);
|
||||
if (target <= 0)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -72,11 +72,27 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
//未落任何层:价格已穿出最深一层边界(低于多头最深层下界/高于空头最深层上界),按最深层计;
|
||||
//最深层按边界值取(多头=最小下界、空头=最大上界),不依赖配置数组顺序(BUG-25 引擎侧防御)
|
||||
return isCustomerLong
|
||||
//未落任何层分两种情形:
|
||||
//① 价格穿出最深一层边界(多头低于最深层下界/空头高于最深层上界)→ 按最深层计(追保金额不再上升);
|
||||
// 最深层按边界值取(多头=最小下界、空头=最大上界),不依赖配置数组顺序(BUG-25 引擎侧防御)。
|
||||
//② 层间空隙(如空头 (0.99,1.00]——价格在期初附近小幅波动、未触发追保的区间)→ 返回 null,追加保证金按 0。
|
||||
// 此前兜底不分情形一律按最深层计,空隙价格被错误收取最深档追保
|
||||
// (2026-08-27 交易2538实证:08-24净价100→ratio 0.99999 落空头(0.99,1.00]空档,被按0.04最深档收80.8万)。
|
||||
var deepest = isCustomerLong
|
||||
? valid.OrderBy(t => t.Lower ?? double.MinValue).First()
|
||||
: valid.OrderByDescending(t => t.Upper ?? double.MaxValue).First();
|
||||
if (isCustomerLong)
|
||||
{
|
||||
if (priceRatio < (deepest.Lower ?? double.MinValue))
|
||||
{
|
||||
return deepest;
|
||||
}
|
||||
}
|
||||
else if (priceRatio > (deepest.Upper ?? double.MaxValue))
|
||||
{
|
||||
return deepest;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -357,6 +357,19 @@ namespace YLErp.Modules.SwapModule
|
||||
directionRatio);
|
||||
}
|
||||
|
||||
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate,
|
||||
decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty)
|
||||
{
|
||||
if (!corporateActionQty.HasValue)
|
||||
{
|
||||
return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
|
||||
}
|
||||
|
||||
var service = new BondPaymentService(UserInfo);
|
||||
var payments = service.GetBondPayments(underlyingCode, fromDate, toDate);
|
||||
return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty);
|
||||
}
|
||||
|
||||
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
|
||||
|
||||
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
|
||||
@@ -439,7 +452,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 获取公司行为公式使用的收盘价。
|
||||
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
|
||||
/// ExDividendDate;不能在 8 月 17 日 EOD 误取 8 月 17 日收盘价重算 8 月 14 日
|
||||
/// ExDividendDate;不能在 除权日 EOD 误取 除权日收盘价重算 登记日
|
||||
/// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。
|
||||
/// </summary>
|
||||
protected virtual decimal GetFundCorporateActionClosePrice(
|
||||
@@ -516,18 +529,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 +552,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 +563,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,22 +608,27 @@ 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);
|
||||
var corporateActionCashDividendBeforePositions = corporateActionBeforePositions
|
||||
.Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||||
&& exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend)
|
||||
&& dividend.GiveCashAmount != 0m)
|
||||
.ToList();
|
||||
|
||||
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
|
||||
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
|
||||
@@ -613,18 +638,20 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 处理浮动腿归档
|
||||
var curEodPosis = DealFloatPositions(
|
||||
floatPositionsForCompose,
|
||||
realPosiList,
|
||||
openingEodPositions,
|
||||
todyEodPositions,
|
||||
settleDate,
|
||||
td,
|
||||
preSettleDate,
|
||||
flowEvents);
|
||||
floatPositionsForCompose, // 初始腿
|
||||
realPosiList, // 实时腿
|
||||
openingEodPositions, // 开盘基线
|
||||
todyEodPositions, // 当日终持仓
|
||||
settleDate, // 收盘日期
|
||||
td, // 交易
|
||||
preSettleDate, // 上一交易日
|
||||
flowEvents, // 流水事件
|
||||
corporateActionCashDividendBeforePositions);
|
||||
|
||||
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
|
||||
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
|
||||
// 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。
|
||||
// 公司行为事件
|
||||
RecordCorporateActionEvents(
|
||||
td,
|
||||
curEodPosis,
|
||||
@@ -632,8 +659,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 +696,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 +757,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 +768,7 @@ namespace YLErp.Modules.SwapModule
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取除权参考价
|
||||
// 获取除权参考价 - 登记日收盘价
|
||||
var corporateActionClosePrice = GetFundCorporateActionClosePrice(
|
||||
dividendInfo,
|
||||
position.UnderlyingPrice);
|
||||
@@ -773,8 +802,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 +932,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 +945,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 +954,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 +1001,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 +1019,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 +1041,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 +1223,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal dividendTaxRate,
|
||||
int grossPriceRound)
|
||||
{
|
||||
// 计算除权系数 - adjustCashDividendPrice = false (现金分红模式)
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
closePrice,
|
||||
@@ -1414,7 +1454,8 @@ namespace YLErp.Modules.SwapModule
|
||||
DateTime settleDate,
|
||||
trade td,
|
||||
DateTime preSettleDate,
|
||||
List<swap_flow_event> flowEvents)
|
||||
List<swap_flow_event> flowEvents,
|
||||
IReadOnlyCollection<eod_swap_position> corporateActionBeforePositions = null)
|
||||
{
|
||||
string settleDateStr = settleDate.ToString("yyyy-MM-dd");
|
||||
string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd");
|
||||
@@ -1436,18 +1477,20 @@ namespace YLErp.Modules.SwapModule
|
||||
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息
|
||||
var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息
|
||||
var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id);
|
||||
var corporateActionBeforeQuantity = corporateActionBeforePositions?
|
||||
.FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity;
|
||||
eod_swap_position eodPosi = new eod_swap_position();
|
||||
if (eodPosition == null)
|
||||
{
|
||||
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents);
|
||||
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||||
}
|
||||
else if (unwindEvents.Count() == 0)
|
||||
{
|
||||
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate);
|
||||
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity);
|
||||
}
|
||||
else
|
||||
{
|
||||
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents);
|
||||
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||||
}
|
||||
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
|
||||
list.Add(eodPosi);
|
||||
@@ -2634,7 +2677,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="todayPositions">当日日终归档信息</param>
|
||||
/// <param name="swap_Deals">当日平仓/互换事件信息</param>
|
||||
/// <param name="td">交易信息</param>
|
||||
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
|
||||
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
if (curretEod == null)
|
||||
{
|
||||
@@ -2656,7 +2699,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal tax = um.ValueAddedTax ?? 0;
|
||||
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||||
{
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||||
}
|
||||
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
|
||||
@@ -2718,7 +2761,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="curretEod"></param>
|
||||
/// <param name="td"></param>
|
||||
/// <param name="valueDate"></param>
|
||||
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
|
||||
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
if (curretEod == null)
|
||||
{
|
||||
@@ -2752,7 +2795,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// 修改,互换事件会影响待实现的分红的,现在要算上
|
||||
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
|
||||
{
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||||
}
|
||||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||||
@@ -2879,7 +2922,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="position"></param>
|
||||
/// <param name="td"></param>
|
||||
/// <param name="settleDate"></param>
|
||||
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
|
||||
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate,
|
||||
DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
eod_swap_position curretEod = new eod_swap_position();
|
||||
var um = GetUnderlyingData(position.UnderlyingCode);
|
||||
@@ -2932,7 +2976,7 @@ namespace YLErp.Modules.SwapModule
|
||||
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||||
{
|
||||
decimal tax = um.ValueAddedTax ?? 0;
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
payment = DividendCalc.AfterTax(payment, tax);
|
||||
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
|
||||
curretEod.TdPosiDividend = payment;
|
||||
@@ -3370,6 +3414,91 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 EQD-7084 新“框架合约”字段。
|
||||
/// 旧查询负责筛选、排序、分页及旧字段计算;新字段只基于当前页对应的日终腿补充计算,
|
||||
/// 避免改变旧接口的返回口径。
|
||||
/// </summary>
|
||||
public SearchListResult<EodSwapRiskNewResponse> SearchEodSwapNewList(EodSwapQueryRequest req)
|
||||
{
|
||||
// 新 Tab 与旧 Tab 共享同一套权限、筛选、排序和分页边界;先复用旧查询,
|
||||
// 再只替换需求明确调整的展示字段,避免新接口悄然改变旧口径或查询范围。
|
||||
var oldResult = SearchEodSwapList(req);
|
||||
var oldRows = oldResult.rows?.ToList() ?? new List<EodSwapResponse>();
|
||||
var tradeIds = oldRows.Select(x => x.position.SwapTradeId).Distinct().ToList();
|
||||
var valueDates = oldRows.Select(x => x.position.ValueDate).Distinct().ToList();
|
||||
|
||||
if (tradeIds.Count == 0)
|
||||
{
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult,
|
||||
Enumerable.Empty<EodSwapRiskNewResponse>());
|
||||
}
|
||||
|
||||
// 当前页的交易、日终明细和扩展信息各批量读取一次,随后在内存按“交易 + 日终日”配对。
|
||||
// 不在 rows.Select 内查询数据库,避免分页结果产生 N+1 查询。
|
||||
var trades = DbContext.trade
|
||||
.Where(x => tradeIds.Contains(x.id))
|
||||
.Select(x => new { x.id, x.StartDate, x.ExerciseDate })
|
||||
.ToDictionary(x => x.id);
|
||||
var eodPositionDetails = DbContext.eod_swap_position
|
||||
.Where(x => tradeIds.Contains(x.SwapTradeId)
|
||||
&& valueDates.Contains(x.ValueDate)
|
||||
&& !x.Invalid)
|
||||
.ToList();
|
||||
var tradeExtends = DbContext.trade_extend
|
||||
.Where(x => tradeIds.Contains(x.TradeId))
|
||||
.ToList();
|
||||
|
||||
var rows = oldRows.Select(item =>
|
||||
{
|
||||
// 同一交易可出现在多个日终日;必须同时匹配 ValueDate,不能把其他日期的腿混入本行。
|
||||
var details = eodPositionDetails
|
||||
.Where(x => x.SwapTradeId == item.position.SwapTradeId
|
||||
&& x.ValueDate == item.position.ValueDate)
|
||||
.ToList();
|
||||
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var interestLegs = details.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
|
||||
// 缺少扩展信息时按“期间支付”处理,和旧接口的默认值保持一致。
|
||||
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
|
||||
trades.TryGetValue(item.position.SwapTradeId, out var tradeInfo);
|
||||
|
||||
return new EodSwapRiskNewResponse
|
||||
{
|
||||
position = item.position,
|
||||
TradeDate = item.TradeDate,
|
||||
SwapTradeNo = item.SwapTradeNo,
|
||||
ClientName = item.ClientName,
|
||||
StructureType = item.StructureType,
|
||||
AssetBookName = item.AssetBookName,
|
||||
ClientId = item.ClientId,
|
||||
SwapTradeTypeStr = item.SwapTradeTypeStr,
|
||||
UnderlyingType = item.UnderlyingType,
|
||||
PeriodAmount = item.PeriodAmount,
|
||||
FloatingUnrealizedPnl = item.FloatingUnrealizedPnl,
|
||||
InterestPaymentMethod = item.InterestPaymentMethod,
|
||||
MaturityNettingValuation = item.MaturityNettingValuation,
|
||||
PeriodPaymentValuation = item.PeriodPaymentValuation,
|
||||
MarginInterestGain = item.MarginInterestGain,
|
||||
MarginInterestLoss = item.MarginInterestLoss,
|
||||
// 所有 EQD-7084 差异集中在 NewFields;上方复制的旧字段用于保留原报表的
|
||||
// 基本信息、DV、期间金额及已实现收益,前端再将六个差异列绑定到 NewFields。
|
||||
NewFields = CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
item.StructureType,
|
||||
item.position.NotionalValue,
|
||||
tradeInfo?.StartDate,
|
||||
tradeInfo?.ExerciseDate,
|
||||
item.PeriodAmount,
|
||||
dividendPayDate)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult, rows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取互换交易日终持仓数据
|
||||
/// </summary>
|
||||
@@ -3448,6 +3577,29 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的字段口径。
|
||||
/// 纯函数只依赖日终浮动腿、利息腿和交易级展示参数,供查询接口及无库单测共用。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? ExerciseDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
=> EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
ExerciseDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
|
||||
/// <summary>
|
||||
/// 互换持仓明细查询
|
||||
/// </summary>
|
||||
@@ -3590,7 +3742,7 @@ namespace YLErp.Modules.SwapModule
|
||||
else if (isEtf)
|
||||
{
|
||||
item.PeriodAmount = null;
|
||||
item.DividendAmount = pendingDividend;
|
||||
item.DividendAmount = -pendingDividend; // 每日估值报告是客户视角 取值与日终持仓风险相反
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3634,9 +3786,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 +3834,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;
|
||||
|
||||
@@ -207,8 +207,8 @@ namespace YLErp.Modules.SwapModule
|
||||
return events;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取交易操作历史。登记日创建但尚未到 EffectiveDate 的公司行为事件也保留,
|
||||
/// 由 EventData.Applied=false 表示“待生效”,保证审计日志完整可追溯。
|
||||
/// 获取交易操作历史。登记日创建的待生效公司行为仍保留在审计数据中,
|
||||
/// 但在 EffectiveDate 将其更新为 Applied=true 前不对操作历史展示。
|
||||
/// </summary>
|
||||
/// <param name="tradeId">交易id</param>
|
||||
/// <returns></returns>
|
||||
@@ -218,7 +218,30 @@ namespace YLErp.Modules.SwapModule
|
||||
.Where(x => x.SwapTradeId == tradeId)
|
||||
.OrderByDescending(o => o.id)
|
||||
.ToList();
|
||||
return list;
|
||||
return FilterOperationHistory(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤尚未生效的公司行为事件。非公司行为、已生效事件和无法识别的历史事件均保留,
|
||||
/// 避免过滤条件误伤既有操作记录。
|
||||
/// </summary>
|
||||
private static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
|
||||
{
|
||||
if (events == null)
|
||||
{
|
||||
return new List<swap_event>();
|
||||
}
|
||||
|
||||
return events
|
||||
.Where(x => !IsPendingCorporateActionEvent(x))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool IsPendingCorporateActionEvent(swap_event swapEvent)
|
||||
{
|
||||
return swapEvent?.EventType == (int)SwapEventTypeEnum.公司行为
|
||||
&& TryDeserializeCorporateActionEventData(swapEvent, out var data)
|
||||
&& !data.Applied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,8 +270,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为说明使用稳定的键值格式,完整保留调整前后名义本金、价格、数量、
|
||||
/// 待实现分红和现金流变化,操作历史无需重新计算即可核对。
|
||||
/// 公司行为说明仅展示调整前后的名义本金、期初标的价格和持仓数量,
|
||||
/// 便于操作历史直接比对持仓基线。
|
||||
/// </summary>
|
||||
public static string BuildCorporateActionEventReason(CorporateActionEventData data)
|
||||
{
|
||||
@@ -257,37 +280,38 @@ 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[]
|
||||
string ActionDescription()
|
||||
{
|
||||
$"公司行为[{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}"
|
||||
});
|
||||
if (data.RationedSharesAmount != 0m)
|
||||
{
|
||||
return "配股";
|
||||
}
|
||||
if (data.GiveShareAmount != 0m)
|
||||
{
|
||||
return "送股";
|
||||
}
|
||||
if (data.Split.HasValue && data.Split.Value != 1m)
|
||||
{
|
||||
return "拆分";
|
||||
}
|
||||
if (data.GiveCashAmount != 0m)
|
||||
{
|
||||
// return $"产生分红:{D(data.CashFlowChange)}";
|
||||
return $"产生分红";
|
||||
}
|
||||
return "公司行为";
|
||||
}
|
||||
|
||||
return $"股权登记日:{Date(data.ExDividendDate)} 发生公司行为({ActionDescription()})"
|
||||
+ Environment.NewLine
|
||||
+ $"调整前:名义本金:{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)
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 标签赋值与返还两个写入口集中在本服务,授信出入表(ClientCreditInoutService)的占用/释放由此统一触发。
|
||||
/// 口径:授信值取 credit.Credit 合计(已审批+日期有效+含母公司,阶段一已折算),已使用授信取授信出入表;
|
||||
/// 授信不进资金——授信部分不产生资金流水。
|
||||
/// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选默认现金),确认成交时系统在同列定稿。
|
||||
/// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选回退交易级
|
||||
/// margin_fund_source,交易级也未设默认现金),确认成交时系统在同列定稿。
|
||||
/// </summary>
|
||||
public class SwapFundTagService : YLBaseService
|
||||
{
|
||||
@@ -50,10 +51,81 @@ namespace YLErp.Modules.SwapModule
|
||||
return GetEffectiveCredit(clientId, valueDate) - ClientCreditInoutService.GetUsedCredit(clientId, DbContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存前授信拆单(§2.3 保存前拆单,2026-08-26 业务确认:保存检查授信→不足拦截→UI 确认→拆完再保存)。
|
||||
/// 按当前剩余授信对偏好授信的预付金腿(腿选授信,或腿默认回退交易级资金来源=授信)做物理拆分:
|
||||
/// 原腿=可用额度 标授信、克隆现金差额腿(插回 td.swap_positions 随保存落库);额度为0/耗尽的授信腿整体定稿现金。
|
||||
/// 有授信不足且未带确认标记(allowSplit=false)时抛 TradeMarginCreditSplitException——
|
||||
/// controller 返回 AdditionalProcessing/MarginCreditSplit 由 UI 确认后带参重提。
|
||||
/// 本方法只拆腿不定簿记:授信占用/资金流水仍在确认成交 ApplyMarginFundTags。
|
||||
/// </summary>
|
||||
public void PreSplitMarginLegsByCredit(trade td, bool allowSplit)
|
||||
{
|
||||
var marginModes = new[] { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
|
||||
var preferLegs = (td.swap_positions ?? new List<swap_position>())
|
||||
.Where(x => marginModes.Contains(x.InterestMode)
|
||||
//不扣本金的腿不产生预付金簿记(与 SwapTradeConfirm 同口径),不参与拆分
|
||||
&& (x.Obervation == null || x.Obervation.IsDeductPrincipal)
|
||||
&& ConsFundTag.PreferCredit(x.FundTag, td.MarginFundSource))
|
||||
.ToList();
|
||||
if (preferLegs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var valueDate = td.TradeDate ?? DateTime.Now;
|
||||
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
|
||||
var allocateLegs = preferLegs
|
||||
.Select(x => new LegAmount
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
PreferCredit = true
|
||||
})
|
||||
.Where(x => x.Amount > 0)
|
||||
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
|
||||
.ThenBy(x => x.Leg.id)
|
||||
.ToList();
|
||||
if (allocateLegs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck: false);
|
||||
//授信不足的腿 = 偏好授信但授信没覆盖全额(含额度为0/被前腿耗尽的整体转现金)
|
||||
var shortPlans = plans.Where(p => p.CreditAmount < p.Amount).ToList();
|
||||
if (shortPlans.Count == 0)
|
||||
{
|
||||
//额度充足:全额授信腿就法定稿授信(含"默认+交易级授信"回退解析),无拆分、无拦截
|
||||
FundTagCalc.ApplySaveTimeSplit(allocateLegs, plans);
|
||||
return;
|
||||
}
|
||||
if (!allowSplit)
|
||||
{
|
||||
var detail = string.Join(";", shortPlans.Select(p => p.NeedSplit
|
||||
? $"金额 {p.Amount:#,##0.00} → 授信 {p.CreditAmount:#,##0.00} + 现金 {p.CashAmount:#,##0.00}"
|
||||
: $"金额 {p.Amount:#,##0.00} → 全额现金(可用授信不足)"));
|
||||
throw new TradeMarginCreditSplitException(
|
||||
$"预付金授信额度不足,剩余可用授信 {Math.Max(creditAvailable, 0):#,##0.00}:{detail}。"
|
||||
+ "确认后将按上述拆分保存(授信部分确认成交时占用授信额度、不产生资金流水;现金部分产生应付预付金)。");
|
||||
}
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(allocateLegs, plans);
|
||||
//新现金腿插回原腿之后(列表相邻,随 SaveSwapPositions 落库并分配 PosiNumber)
|
||||
var splitPlans = plans.Where(p => p.NeedSplit).ToList();
|
||||
for (var i = 0; i < newLegs.Count; i++)
|
||||
{
|
||||
newLegs[i].OptId = UserId;
|
||||
newLegs[i].OptName = UserName;
|
||||
newLegs[i].OptTime = DateTime.Now;
|
||||
var original = splitPlans[i].Leg;
|
||||
var index = td.swap_positions.IndexOf(original);
|
||||
td.swap_positions.Insert(index < 0 ? td.swap_positions.Count : index + 1, newLegs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。
|
||||
/// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿——
|
||||
/// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。
|
||||
/// 特批全现金;按授信分配(腿选授信,或腿未选回退交易级 margin_fund_source=授信)的腿按剩余额度占用,
|
||||
/// 跨界腿拆单为 授信+现金 两条(原腿保留授信部分、差额拆出新现金腿);现金直接现金。
|
||||
/// 授信腿只写授信出入表占用(占用记正数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。
|
||||
/// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。
|
||||
@@ -74,7 +146,9 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
PreferCredit = x.FundTag == ConsFundTag.Credit
|
||||
//优先级:腿上显式选择 > 交易级 margin_fund_source 回退(§2.3 情形1)> 默认现金,
|
||||
//与 TradeCanBeConfirm 校验分流共用 ConsFundTag.PreferCredit 保证口径一致
|
||||
PreferCredit = ConsFundTag.PreferCredit(x.FundTag, td.MarginFundSource)
|
||||
})
|
||||
.Where(x => x.Amount > 0)
|
||||
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
|
||||
@@ -82,12 +156,12 @@ namespace YLErp.Modules.SwapModule
|
||||
.ToList();
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck);
|
||||
|
||||
//先落库拆分的新腿(需要 id 才能绑定占用记录)
|
||||
//先落库拆分的新现金腿(需要 id 才能绑定现金流水)
|
||||
foreach (var plan in plans.Where(p => p.NeedSplit))
|
||||
{
|
||||
plan.CreditLeg = SplitLeg(td, plan);
|
||||
plan.CashLeg = SplitLeg(td, plan);
|
||||
}
|
||||
//标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别标 Cash/Credit;
|
||||
//标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别定稿(原腿=授信、新腿=现金);
|
||||
//整腿授信→Credit、整腿现金/负应付(客户净收取)腿→Cash
|
||||
foreach (var leg in marginLegs)
|
||||
{
|
||||
@@ -110,26 +184,27 @@ namespace YLErp.Modules.SwapModule
|
||||
var happenDate = leg.HappenDate ?? td.TradeDate ?? DateTime.Now;
|
||||
if (plan != null && plan.CreditAmount > 0)
|
||||
{
|
||||
//整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。
|
||||
//授信部分(整腿授信 或 拆单后保留在原腿的可用额度部分):不产生资金流水,只写占用(占用绑原腿)。
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
creditService.Occupy(td.ClientId, leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用");
|
||||
}
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,现金部分按差额产生
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,
|
||||
//现金部分按差额产生——拆单腿的流水绑新拆出的现金腿,整腿现金/负应付腿绑原腿
|
||||
var recordAmount = plan != null
|
||||
? -plan.CashAmount
|
||||
: Convert.ToDouble(leg.InterestPrincipalFix * (leg.InterestDirection == 1 ? -1 : 1));
|
||||
if (recordAmount != 0)
|
||||
{
|
||||
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, leg.id, ClientCashInCashOut.系统操作_应付预付金);
|
||||
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, plan?.CashLeg?.id ?? leg.id, ClientCashInCashOut.系统操作_应付预付金);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留现金部分并标 Cash(资金来源同步改现金,与最终标签一致),
|
||||
/// 克隆一条授信腿(InterestPrincipalFix 按授信金额折算)标 Credit,返回新腿。
|
||||
/// 拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。
|
||||
/// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留授信部分(可用额度)并标 Credit(占用记录绑原腿),
|
||||
/// 克隆一条现金腿(授信不足的差额,InterestPrincipalFix 按现金金额折算)标 Cash,返回新腿
|
||||
/// (现金流水绑新腿)。拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。
|
||||
/// </summary>
|
||||
private swap_position SplitLeg(trade td, LegFundPlan plan)
|
||||
{
|
||||
@@ -137,22 +212,22 @@ namespace YLErp.Modules.SwapModule
|
||||
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
|
||||
var payableRatio = leg.InterestDirection == 1 ? 1 : -1;
|
||||
var originalFix = leg.InterestPrincipalFix;
|
||||
leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CashAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
leg.FundTag = ConsFundTag.Cash;
|
||||
leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
leg.FundTag = ConsFundTag.Credit;
|
||||
|
||||
var creditLeg = leg.Clone();
|
||||
creditLeg.id = 0;
|
||||
creditLeg.PositionId = 0;
|
||||
//授信腿倒挤 = 原 fix − 现金 fix(BUG-20:两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
creditLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
creditLeg.FundTag = ConsFundTag.Credit;
|
||||
creditLeg.OptId = UserId;
|
||||
creditLeg.OptName = UserName;
|
||||
creditLeg.OptTime = DateTime.Now;
|
||||
DbContext.swap_position.Add(creditLeg);
|
||||
var cashLeg = leg.Clone();
|
||||
cashLeg.id = 0;
|
||||
cashLeg.PositionId = 0;
|
||||
//现金腿倒挤 = 原 fix − 授信 fix(两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
cashLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
cashLeg.FundTag = ConsFundTag.Cash;
|
||||
cashLeg.OptId = UserId;
|
||||
cashLeg.OptName = UserName;
|
||||
cashLeg.OptTime = DateTime.Now;
|
||||
DbContext.swap_position.Add(cashLeg);
|
||||
DbContext.SaveChanges();
|
||||
creditLeg.PosiNumber = $"{td.TradeNumber}-{creditLeg.id}";
|
||||
return creditLeg;
|
||||
cashLeg.PosiNumber = $"{td.TradeNumber}-{cashLeg.id}";
|
||||
return cashLeg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -86,8 +86,19 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="td"></param>
|
||||
/// <param name="ignoreMoneyCheck"></param>
|
||||
/// <returns></returns>
|
||||
public trade SaveTrade(trade req, bool ignoreMoneyCheck = false)
|
||||
public trade SaveTrade(trade req, bool ignoreMoneyCheck = false, bool allowMarginCreditSplit = false)
|
||||
{
|
||||
//资金来源必填(现金/授信,默认现金):保存前归一,兜住 DMA 自动建仓等绕过录入页的链路
|
||||
if (req.TradeType == "收益互换" && string.IsNullOrWhiteSpace(req.MarginFundSource))
|
||||
{
|
||||
req.MarginFundSource = ConsFundTag.Cash;
|
||||
}
|
||||
// §2.3 保存前授信拆单(2026-08-26 业务确认):保存检查授信→不足拦截(UI 确认)→拆完再保存。
|
||||
// 特批(ignoreMoneyCheck)语义为全现金不占授信,跳过拆单
|
||||
if (!ignoreMoneyCheck && req.TradeType == "收益互换")
|
||||
{
|
||||
new SwapFundTagService(this).PreSplitMarginLegsByCredit(req, allowMarginCreditSplit);
|
||||
}
|
||||
var um = checkUnderlying(req);
|
||||
trade dbTrade = new trade();
|
||||
//交易保存处理(PrepareInitialMargin 在此把 trade_Initial_Margin 折算进 req.InitialMargin,
|
||||
@@ -1709,7 +1720,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
fundTagSvc.ApplyMarginFundTags(td, generateMarginLegs, cashSvc, false);
|
||||
//标签定稿(含可能的拆单)后重克隆实时持仓:TradeBack 的克隆先于定稿生成,
|
||||
//重克隆使实时腿继承定稿标签、新拆出的授信腿也获得克隆(平仓返还分流查的是实时腿标签)
|
||||
//重克隆使实时腿继承定稿标签、拆单新拆出的现金腿也获得克隆(平仓返还分流查的是实时腿标签)
|
||||
InitialPosition(td);
|
||||
// 合约维度盯市+无预付金腿:重建交易级(positionId=0)初始预付金记录(与 SwapTradeConfirm 一致,回退重补场景)。
|
||||
// 有预付金腿的互换由上面按腿重建,不在此重复生成。
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using BaseOUDAL;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存交易时预付金授信不足的标记异常(§2.3 保存前拆单,2026-08-26 业务确认):
|
||||
/// 保存检查授信→不足拦截→UI 确认(AdditionalProcessing/MarginCreditSplit)→带参重提后
|
||||
/// 按剩余授信物理拆分预付金腿(原腿=可用额度 标授信、新腿=差额 标现金)再保存。
|
||||
/// 属标准业务流程,不受"允许交易特批"开关控制(与 LackOfMoney 特批协议区分)。
|
||||
/// </summary>
|
||||
public class TradeMarginCreditSplitException : ServiceException
|
||||
{
|
||||
public TradeMarginCreditSplitException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user