#EQD-6948 国联民生-实现保证金规则(2)追保金额的产生与收盘计算 收盘计算和授信占用
This commit is contained in:
@@ -6,12 +6,20 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 客户授信出入服务(R4 授信/现金标签体系,决策④)。
|
||||
/// 授信出入表的写入口集中在 标签赋值(占用)与 平仓/到期返还(释放)两个链路内,禁止散落调用。
|
||||
/// 金额符号口径(2026-08-20 业务确认):与资金流水同号——入金为负、出金为正;
|
||||
/// 已使用授信 = SUM(amount) 直接求和(入金使已使用授信下降、可用授信=有效授信−已使用授信 上升;出金反之)。
|
||||
/// 金额符号口径(BUG-01 修正,2026-08-24):**占用记正数、释放记负数**(与 §2.2 原表"占用/调整为正"一致)——
|
||||
/// 已使用授信 = SUM(amount) 直接求和(占用使其上升、释放使其回落),可用授信 = 有效授信 − 已使用授信(占用使其收缩)。
|
||||
/// 2026-08-20 曾裁定"与资金流水同号(入金负/出金正)"——但授信不入资金流水(§2.1),同号对齐的对手方记录并不存在,
|
||||
/// 且该口径下"占用越多已使用授信越负、可用授信越大"(越占越多),已废弃;历史同号行由迁移脚本翻符号。
|
||||
/// change_type(占用/释放/调整)仅作分类审计,不参与求和方向。
|
||||
/// </summary>
|
||||
public class ClientCreditInoutService : YLBaseService
|
||||
{
|
||||
/// <summary>
|
||||
/// 追加保证金相关授信出入记录的 remark 前缀(阶段四 §4.1 EOD 占用写入"追加保证金占用";
|
||||
/// 累计保证金/缺口口径与清理链路以此识别,后续如写释放类记录沿用同一前缀)。
|
||||
/// </summary>
|
||||
public const string AdditionalMarginRemark = "追加保证金";
|
||||
|
||||
public ClientCreditInoutService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
}
|
||||
@@ -21,9 +29,19 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户已使用授信 = SUM(amount)(金额与资金流水同号:入金负、出金正)。
|
||||
/// 可用授信 = 有效授信 − 本值;入金使可用授信增长、出金使其收缩。
|
||||
/// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)等静态上下文直接调用。
|
||||
/// 是否追加保证金相关记录(remark 前缀标识,阶段四 EOD 写入)。
|
||||
/// </summary>
|
||||
public static bool IsAdditionalMarginRecord(client_credit_inout record)
|
||||
{
|
||||
return record != null && !string.IsNullOrEmpty(record.remark)
|
||||
&& record.remark.StartsWith(AdditionalMarginRemark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户已使用授信 = SUM(amount)(占用为正、释放为负)。
|
||||
/// 可用授信 = 有效授信 − 本值:占用使其收缩、释放使其回升。
|
||||
/// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)、拆单额度(GetAvailableCredit)、
|
||||
/// 估值报告可用资金(阶段三 §3.2)等消费方统一调用。
|
||||
/// </summary>
|
||||
public static double GetUsedCredit(int clientId, YLContext db)
|
||||
{
|
||||
@@ -40,8 +58,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入一条授信变化记录(占用/释放/调整统一入口),并冗余记录变更后已使用授信。
|
||||
/// amount 带符号:入金(客户付)为负、出金(客户收)为正,与资金流水 Money 同号。
|
||||
/// 写入一条授信变化记录(统一入口),并冗余记录变更后已使用授信。
|
||||
/// amount 带符号直写(人工调整类可正可负);占用/释放请走 Occupy/Release 语义入口(内部定方向)。
|
||||
/// </summary>
|
||||
public client_credit_inout Record(int clientId, long? positionId, int? tradeId, int changeType,
|
||||
double amount, DateTime happenDate, string remark)
|
||||
@@ -51,7 +69,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return null;
|
||||
}
|
||||
var amountRounded = Math.Round(amount, 2, MidpointRounding.AwayFromZero);
|
||||
//变更后已使用授信快照:直接求和口径(符号已含方向)
|
||||
//变更后已使用授信快照:直接求和口径(占用正/释放负,符号已含方向)
|
||||
var usedAfter = Math.Round(GetUsedCredit(clientId) + amountRounded, 2, MidpointRounding.AwayFromZero);
|
||||
var record = new client_credit_inout
|
||||
{
|
||||
@@ -74,31 +92,37 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
/// <summary>
|
||||
/// 占用:预付金腿标 Credit 的簿记入金(含拆单的授信部分;交易级占用 positionId 为空)。
|
||||
/// amount 传负数(入金方向,与资金流水同号)。
|
||||
/// amount 传正数(占用数量),内部记正——已使用授信随之上升。
|
||||
/// </summary>
|
||||
public client_credit_inout Occupy(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark)
|
||||
{
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeOccupy, amount, happenDate, remark);
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeOccupy, Math.Abs(amount), happenDate, remark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放:平仓/到期按原标签返还授信部分(按腿的 position_id 匹配原占用记录)。
|
||||
/// amount 传正数(出金方向,与资金流水同号)。
|
||||
/// amount 传正数(释放数量),内部记负——已使用授信随之回落。
|
||||
/// </summary>
|
||||
public client_credit_inout Release(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark)
|
||||
{
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeRelease, amount, happenDate, remark);
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeRelease, -Math.Abs(amount), happenDate, remark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除某交易的全部授信出入记录:与资金记录同生命周期——
|
||||
/// 删除某交易的授信出入记录:与资金记录同生命周期——
|
||||
/// 交易回退到开仓(DeleteTradeCashInCashOut)、修改清除(ClearSwapPositions)、删除交易时同步清理,
|
||||
/// 重新确认/重补时按最新标签重写,避免占用悬挂。
|
||||
/// 注意:阶段四追加保证金占用落地后,此处需区分保留追加部分。
|
||||
/// keepAdditionalMargin=true 时保留追加保证金部分(阶段四落地):重确认自愈/修改清除只重写 应付预付金 相关占用,
|
||||
/// 追加部分由 EOD 幂等维护(该两处不删除追加资金记录,授信占用须同步保留,否则已使用授信被低估);
|
||||
/// 删除交易(资金记录全删)传 false 全清。
|
||||
/// </summary>
|
||||
public void RemoveByTrade(int tradeId)
|
||||
public void RemoveByTrade(int tradeId, bool keepAdditionalMargin = false)
|
||||
{
|
||||
var records = DbContext.client_credit_inout.Where(x => x.trade_id == tradeId).ToList();
|
||||
if (keepAdditionalMargin)
|
||||
{
|
||||
records = records.Where(x => !IsAdditionalMarginRecord(x)).ToList();
|
||||
}
|
||||
DbContext.client_credit_inout.RemoveRange(records);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1 合约维度追加保证金的纯函数(金额判定与授信/现金分配,DB 胶水在 SwapAdditionalMarginService)。
|
||||
/// 口径(实现方案 §0 基线 + 需求拆分 R3):
|
||||
/// - 累计保证金(净收取为正)= 该交易 应付预付金+追加保证金 资金记录收付净额 + 追加保证金的授信占用部分(授信不进资金、无流水);
|
||||
/// - 目标追加保证金(累计到当日)= 维持保证金 − 应付预付金净收额,≤0 时为 0(追保回落不返还,负缺口在可用资金公式体现);
|
||||
/// - 已补足额 = 追加资金记录累计值(单条 upsert 存累计值,Money 为负)+ 授信占用累计(amount 为负);
|
||||
/// - 当日新增 = 目标 − 已补足,>0 时按阶段二 §2.3 授信优先逻辑分配:授信部分只写授信出入表,差额走资金记录。
|
||||
/// 幂等:同一结算日重跑时 目标/已补足 均不变 → 新增=0,不产生任何写入。
|
||||
/// </summary>
|
||||
public static class SwapAdditionalMarginCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 目标追加保证金(累计到当日,净收取为正)= 维持保证金 − 应付预付金净收额;差值 ≤0 返回 0(无需追加;
|
||||
/// 维持为负=我方净支付方向,同样不追)。
|
||||
/// </summary>
|
||||
public static double CalcTarget(double maintenanceMargin, double payableMarginNet)
|
||||
{
|
||||
return Round(Math.Max(maintenanceMargin - payableMarginNet, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当日新增追加保证金的授信/现金分配(阶段二 §2.3 授信优先的交易级版本):
|
||||
/// 授信部分 = min(新增, 剩余可用授信),差额为现金部分;新增 ≤0 时两者皆 0(不产生)。
|
||||
/// </summary>
|
||||
public static (double CreditAmount, double CashAmount) Allocate(double increment, double creditAvailable)
|
||||
{
|
||||
increment = Round(increment);
|
||||
if (increment <= 0)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
var credit = Math.Min(increment, Round(Math.Max(creditAvailable, 0)));
|
||||
return (credit, Round(increment - credit));
|
||||
}
|
||||
|
||||
private static double Round(double value)
|
||||
{
|
||||
return Math.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
using YLErp.Modules.TradeModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"资金记录。
|
||||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 累计保证金(应付预付金+追加保证金 流水净额 + 追加授信占用);
|
||||
/// 现金部分为逐结算日增量记录(BUG-03 修正:每结算日一条、Money=−increment,键 TradeId+Action+Deal+HappenDate 幂等),
|
||||
/// 需求上升只增不减;授信优先(阶段二规则):授信部分只写授信出入表(remark 前缀=追加保证金,position_id 空、冗余 trade_id)。
|
||||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算",重跑时 目标/已补足 不变 → 新增为 0 不重复写。
|
||||
/// 客户维度(MarginWatchRule=1/NULL)不产生资金记录(§0 占用口径),不在本服务范围。
|
||||
/// </summary>
|
||||
public class SwapAdditionalMarginService : YLBaseService
|
||||
{
|
||||
public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public SwapAdditionalMarginService(YLBaseService baseService) : base(baseService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
|
||||
/// </summary>
|
||||
public void SettleAdditionalMargin(DateTime settleDate, List<int> clientFilter = null)
|
||||
{
|
||||
//合约维度盯市客户
|
||||
var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking()
|
||||
.Where(t => t.ProcessStatus != "未提交" && t.MarginWatchRule == 0)
|
||||
.Select(t => t.id)
|
||||
.ToList();
|
||||
if (clientFilter != null && clientFilter.Any())
|
||||
{
|
||||
watchClientIds = watchClientIds.Where(t => clientFilter.Contains(t)).ToList();
|
||||
}
|
||||
if (watchClientIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结)
|
||||
var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed;
|
||||
var trades = DbContext.trade.AsNoTracking()
|
||||
.Where(t => tradeStatuses.Contains(t.TradeStatus)
|
||||
&& t.ValidState != ConsGlobal.InValid
|
||||
&& t.TradeType == "收益互换"
|
||||
&& t.TradeDate <= settleDate
|
||||
&& watchClientIds.Contains(t.ClientId))
|
||||
.ToList();
|
||||
if (trades.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)——
|
||||
//交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算
|
||||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext);
|
||||
trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
|
||||
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构).ToList();
|
||||
var tradeIds = trades.Select(t => t.id).ToList();
|
||||
if (tradeIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//当日维持保证金(引擎产出,我方净收取为正),按交易合计(与 SwapSpanBalanceQueryService 缺口口径一致)
|
||||
var maintenanceByTrade = DbContext.trade_span.AsNoTracking()
|
||||
.Where(x => x.ValueDate == settleDate && tradeIds.Contains(x.TradeId) && x.Spv != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key, Spv = g.Sum(x => x.Spv ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Spv);
|
||||
|
||||
//应付预付金净收额(客户付钱记负 → 取反为正;平仓返还自动冲减;口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||||
var payableNetByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_应付预付金
|
||||
&& x.HappenDate <= settleDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
//追加保证金资金记录累计值(逐日增量记录求和即累计,BUG-03;Deal=0 交易级,口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||||
var addRecordByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Deal == 0
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||||
|
||||
//追加保证金授信占用累计(amount 占用记正 → 直接求和(BUG-01 修正口径);remark 前缀标识,见 ClientCreditInoutService)
|
||||
var addCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
|
||||
.Where(x => x.trade_id != null && tradeIds.Contains(x.trade_id ?? 0)
|
||||
&& 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);
|
||||
//客户剩余可用授信逐笔扣减缓存(同一次结算内多笔追加按顺序消耗额度,与阶段二逐腿分配同语义)
|
||||
var creditRemaining = new Dictionary<int, double>();
|
||||
|
||||
foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key))
|
||||
{
|
||||
foreach (var td in clientGroup.OrderBy(t => t.id))
|
||||
{
|
||||
if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance,
|
||||
payableNetByTrade.TryGetValue(td.id, out var payableNet) ? payableNet : 0);
|
||||
if (target <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0;
|
||||
var fundedCredit = addCreditByTrade.TryGetValue(td.id, out var credit) ? credit : 0;
|
||||
var increment = Math.Round(target - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
if (increment <= 0)
|
||||
{
|
||||
//已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!creditRemaining.TryGetValue(td.ClientId, out var remain))
|
||||
{
|
||||
remain = fundTagService.GetAvailableCredit(td.ClientId, settleDate);
|
||||
creditRemaining[td.ClientId] = remain;
|
||||
}
|
||||
var (creditPart, cashPart) = SwapAdditionalMarginCalc.Allocate(increment, remain);
|
||||
if (creditPart > 0)
|
||||
{
|
||||
//授信部分不产生资金流水,只写授信出入表占用(占用记正数,BUG-01 修正口径)
|
||||
creditService.Occupy(td.ClientId, null, td.id, creditPart, settleDate,
|
||||
ClientCreditInoutService.AdditionalMarginRemark + "占用");
|
||||
creditRemaining[td.ClientId] = Math.Round(remain - creditPart, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (cashPart > 0)
|
||||
{
|
||||
//现金部分按结算日逐笔增量记录(BUG-03 修正:每结算日一条、Money=−increment、键含日期幂等),
|
||||
//避免单条累计值覆盖 + HappenDate 前移使 EOD 差分窗口跨日全额重复计入;负数=客户应付追加
|
||||
cashService.SaveSwapTradeClientCash(td, -cashPart, settleDate, 0,
|
||||
ClientCashInCashOut.系统操作_追加保证金, matchDate: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 可用资金公式(需求拆分 R2 口径,实时/EOD/报告三处共用保证一致)。
|
||||
/// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例);
|
||||
/// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除);
|
||||
/// 已使用授信 = 授信出入表 Σ(amount);
|
||||
/// 初始保证金(净收取为正)= 应付预付金流水收付净额(平仓返还自动冲减);
|
||||
/// 维持保证金(净收取为正)= −MySideMargin(client_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。
|
||||
/// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。
|
||||
/// </summary>
|
||||
public static class SwapSpanBalanceCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 可用资金(客户维度,MarginWatchRule=1):
|
||||
/// Max(现金结存 + 授信额度 − 已使用授信 + 初始保证金 − 维持保证金, 0)(现金结存已扣初始保证金,故加回)。
|
||||
/// maintenanceMargin/initialMargin 均为净收取为正;未配置维度的存量客户不走本公式(调用方保证)。
|
||||
/// </summary>
|
||||
public static double CalcClientDimensionAvailable(double cashBalance, double totalCredit, double usedCredit,
|
||||
double initialMargin, double maintenanceMargin)
|
||||
{
|
||||
return Math.Max(cashBalance + totalCredit - usedCredit + initialMargin - maintenanceMargin, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可用资金(合约维度,MarginWatchRule=0):
|
||||
/// Max(现金结存 + 授信额度 − 已使用授信 − 交易维度追加保证金合计, 0);
|
||||
/// 交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金)(阶段三:累计=应付预付金净额;阶段四含追加保证金流水)。
|
||||
/// </summary>
|
||||
public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit,
|
||||
double tradeAdditionalMarginSum)
|
||||
{
|
||||
return Math.Max(cashBalance + totalCredit - usedCredit - tradeAdditionalMarginSum, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保金额(客户维度,MarginWatchRule=1,阶段四 §4.2 双向——不以 0 截断):
|
||||
/// (维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信),正=需追保、负=可返还。
|
||||
/// 即客户维度可用资金公式的反向值(去 Max 截断)——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数,
|
||||
/// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。
|
||||
/// </summary>
|
||||
public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit,
|
||||
double initialMargin, double maintenanceMargin)
|
||||
{
|
||||
return Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保金额(合约维度,MarginWatchRule=0,阶段四 §4.2):−(现金结存 + 授信额度 − 已使用授信)。
|
||||
/// 需求原文公式为 现金结存+授信额度−已使用授信(与"交易维度追加保证金"的关系为开放问题2,两值均有产出),
|
||||
/// 此处取负对齐字段"正数=应追加"口径:账户透支(现金+授信不足)为正=应补足,盈余为负。
|
||||
/// </summary>
|
||||
public static double CalcContractDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit)
|
||||
{
|
||||
return Math.Round(-(cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 估值报告/可用资金查询输入(静态查询,供 ClientBalanceUtility 与 RealTimeClientBanlanceService 共用,保证三处口径一致)。
|
||||
/// 口径:
|
||||
/// - 互换初始保证金(净收取为正)= 客户 应付预付金 流水收付净额取反(客户应付入金记负、平仓返还为正,取负号后净收取为正);
|
||||
/// - 交易维度追加保证金(合约维度)= Σ(维持保证金 − 累计保证金):
|
||||
/// 维持保证金取当日 trade_span.Spv(区间追保结构引擎产出,我方净收取为正);
|
||||
/// 累计保证金 = 该交易 应付预付金+追加保证金 流水收付净额 + 追加保证金授信占用净额(§0 口径,阶段四 §4.1 起);
|
||||
/// 仅统计规则15(区间追保结构,R1 三层级解析,与引擎同口径)且有当日 trade_span 的交易。
|
||||
/// </summary>
|
||||
public static class SwapSpanBalanceQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。
|
||||
/// </summary>
|
||||
public static Dictionary<int, double> GetSwapInitMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
|
||||
{
|
||||
if (clientIds == null || clientIds.Count == 0)
|
||||
{
|
||||
return new Dictionary<int, double>();
|
||||
}
|
||||
|
||||
var flows = db.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => clientIds.Contains(x.ClientId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_应付预付金
|
||||
&& x.HappenDate <= valueDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.Select(x => new { ClientId = x.ClientId ?? 0, Money = x.Money ?? 0d })
|
||||
.ToList();
|
||||
|
||||
return flows.GroupBy(x => x.ClientId)
|
||||
.ToDictionary(g => g.Key, g => -g.Sum(x => x.Money));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合约维度输入:交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金),按客户汇总。
|
||||
/// </summary>
|
||||
public static Dictionary<int, double> GetTradeAdditionalMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
|
||||
{
|
||||
var result = new Dictionary<int, double>();
|
||||
if (clientIds == null || clientIds.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
//当日维持保证金(引擎产出:我方净收取为正)——先取当日有 span 的客户交易,再按规则15过滤
|
||||
var maintenance = db.trade_span.AsNoTracking()
|
||||
.Where(x => x.ValueDate == valueDate && x.Spv != null
|
||||
&& x.ClientId != null && clientIds.Contains(x.ClientId.Value))
|
||||
.Select(x => new { x.TradeId, x.ClientId, Spv = x.Spv ?? 0d })
|
||||
.ToList();
|
||||
if (maintenance.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var spanTradeIds = maintenance.Select(x => x.TradeId).Distinct().ToList();
|
||||
|
||||
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/结算判定同口径)——
|
||||
//交易绑定→客户默认→全局默认 找到即停;无预付金等其他规则产出/留存的 span 行不计入追保缺口
|
||||
var spanTrades = db.trade.AsNoTracking().Where(t => spanTradeIds.Contains(t.id)).ToList();
|
||||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(spanTrades, valueDate, db);
|
||||
var rule15TradeIds = spanTrades
|
||||
.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
|
||||
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构)
|
||||
.Select(t => t.id)
|
||||
.ToHashSet();
|
||||
maintenance = maintenance.Where(x => rule15TradeIds.Contains(x.TradeId)).ToList();
|
||||
if (maintenance.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
//累计保证金:该交易 应付预付金+追加保证金 流水收付净额取反(收取为正);
|
||||
//追加保证金的授信占用部分不产生资金流水(阶段二口径),阶段四 §4.1 起一并计入——
|
||||
//否则结算后缺口残留(=授信部分),与可用资金公式里的 −已使用授信 形成双扣
|
||||
var marginActions = new List<string> { ClientCashInCashOut.系统操作_应付预付金, ClientCashInCashOut.系统操作_追加保证金 };
|
||||
var accumulated = db.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && spanTradeIds.Contains(x.TradeId ?? 0)
|
||||
&& marginActions.Contains(x.Action)
|
||||
&& x.HappenDate <= valueDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
//追加保证金的授信占用净额(amount 占用记正 → 直接求和(BUG-01 修正口径);remark 前缀标识)
|
||||
var addCreditOccupied = db.client_credit_inout.AsNoTracking()
|
||||
.Where(x => x.trade_id != null && spanTradeIds.Contains(x.trade_id ?? 0)
|
||||
&& x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark))
|
||||
.GroupBy(x => x.trade_id)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = g.Sum(x => x.amount) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
foreach (var group in maintenance.GroupBy(x => x.ClientId ?? 0))
|
||||
{
|
||||
var total = group.Sum(x => x.Spv
|
||||
- (accumulated.TryGetValue(x.TradeId, out var acc) ? acc : 0d)
|
||||
- (addCreditOccupied.TryGetValue(x.TradeId, out var occupied) ? occupied : 0d));
|
||||
result[group.Key] = total;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 区间追保结构(规则15)维持保证金纯函数计算(实现方案阶段三 §3.1)。
|
||||
/// 数据来源约定(与阶段一 SpanConfig 结构、docx 确认书模板一致):
|
||||
/// - 多头(客户看多):第1层 [Lower, +∞)(上不封顶),第n层 [Lower, Upper);各层 Lower 逐层严格递减;
|
||||
/// - 空头(客户看空):第1层 (−∞, Upper](下不设限),第n层 (Lower, Upper];各层 Upper 逐层严格递增;
|
||||
/// - 区间边界为"×期初净价的百分比"小数(0.95=95%),价格口径:债券用净价,指数/ETF 用收盘价;
|
||||
/// - AmountRate 为累计到该层的追保金额比例(与确认书追保表每行"合计追保金额"同口径,直取不求和),
|
||||
/// 追保金额 = AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF);
|
||||
/// - 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1);
|
||||
/// - 价格跌破最深一层(平仓线外)按最深层计(追保金额不再上升)。
|
||||
/// 公式参数化(EQD-6948 待业务校验),所有口径集中在本类便于校验后调整。
|
||||
/// </summary>
|
||||
public static class SwapSpanMarginCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断 SpanConfig 是否已按方案B录入新区间结构(任一方向有边界/比例或预警/平仓线)。
|
||||
/// 全空视为存量 x/y 配置,引擎回落旧 名义×y 公式。
|
||||
/// </summary>
|
||||
public static bool HasSpanConfig(SpanConfig cfg)
|
||||
{
|
||||
if (cfg == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return cfg.WarnLine.HasValue || cfg.CloseLine.HasValue
|
||||
|| HasTierValue(cfg.LongSpans) || HasTierValue(cfg.ShortSpans);
|
||||
}
|
||||
|
||||
private static bool HasTierValue(List<SpanTierConfig> tiers)
|
||||
{
|
||||
return tiers != null && tiers.Any(t => t != null && (t.Lower.HasValue || t.Upper.HasValue || t.AmountRate.HasValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户方向判定(与确认书 IsCustomerLong 同规则):
|
||||
/// 我方方向 = 收取端(PosiDirection=收取)与 PositionType 同向、支付端反向;客户方向取反。
|
||||
/// </summary>
|
||||
public static bool IsCustomerLong(int posiDirection, int positionType)
|
||||
{
|
||||
var isOurLong = posiDirection == (int)SwapDirectionEnum.支付
|
||||
? positionType == (int)PositionTypeFlag.Short
|
||||
: positionType == (int)PositionTypeFlag.Long;
|
||||
return !isOurLong;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前价格相对期初价的比例落档。返回命中的层;价格在所有层区间之外(平仓线外)返回最深一层,无可用层返回 null。
|
||||
/// 多头:priceRatio ∈ [Lower, Upper)(第1层无上界);空头:priceRatio ∈ (Lower, Upper](第1层无下界)。
|
||||
/// </summary>
|
||||
public static SpanTierConfig MatchTier(List<SpanTierConfig> tiers, bool isCustomerLong, double priceRatio)
|
||||
{
|
||||
if (tiers == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var valid = tiers.Where(t => t != null && (t.Lower.HasValue || t.Upper.HasValue)).ToList();
|
||||
if (valid.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (var tier in valid)
|
||||
{
|
||||
var inLower = !tier.Lower.HasValue || (isCustomerLong ? priceRatio >= tier.Lower.Value : priceRatio > tier.Lower.Value);
|
||||
var inUpper = !tier.Upper.HasValue || (isCustomerLong ? priceRatio < tier.Upper.Value : priceRatio <= tier.Upper.Value);
|
||||
if (inLower && inUpper)
|
||||
{
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
//未落任何层:价格已穿出最深一层边界(低于多头最深层下界/高于空头最深层上界),按最深层计;
|
||||
//最深层按边界值取(多头=最小下界、空头=最大上界),不依赖配置数组顺序(BUG-25 引擎侧防御)
|
||||
return isCustomerLong
|
||||
? valid.OrderBy(t => t.Lower ?? double.MinValue).First()
|
||||
: valid.OrderByDescending(t => t.Upper ?? double.MaxValue).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总追加保证金 = 命中层 AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF)。
|
||||
/// 数据上两者同为 期初价(PosiGrossPrice) × 数量(PosiQuantity),口径差异由调用方注释说明。
|
||||
/// </summary>
|
||||
public static double CalcAdditionalMargin(SpanTierConfig tier, double initPrice, double quantity)
|
||||
{
|
||||
if (tier == null || !tier.AmountRate.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return tier.AmountRate.Value * initPrice * quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1)。
|
||||
/// </summary>
|
||||
public static double CalcMaintenanceMargin(double initialMargin, double additionalMargin, double direction)
|
||||
{
|
||||
return (initialMargin + additionalMargin) * direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三 §3.1 单笔交易引擎计算(纯函数,收盘价由调用方解析后传入——债券取中债估值净价、指数/ETF取收盘价)。
|
||||
/// closePrice<=0 视为未取到收盘价:追加保证金按 0、维持保证金=初始保证金(不抛错,由调用方记日志)。
|
||||
/// isInitialCalc=true(试算初始):只出初始项,追加保证金为收盘后口径不参与。
|
||||
/// 返回 null 表示缺有效标的腿(期初价),调用方跳过该交易不产出 trade_span。
|
||||
/// </summary>
|
||||
public static double? CalcTradeMaintenanceMargin(double? tradeInitialMargin, SpanConfig spanCfg,
|
||||
List<swap_position> legs, bool isInitialCalc, double closePrice)
|
||||
{
|
||||
//标的腿(多空):期初价格、数量、客户方向
|
||||
var underlyingLeg = legs?.FirstOrDefault(x => x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short);
|
||||
if (underlyingLeg == null || underlyingLeg.PosiGrossPrice <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var isCustomerLong = IsCustomerLong(underlyingLeg.PosiDirection, underlyingLeg.PositionType);
|
||||
|
||||
//初始保证金与方向:初始预付金腿(InterestMode=5)收付净额(多腿按净收取定方向);无腿时回落交易录入值(客户应付常态)
|
||||
var initMarginLegs = legs.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
|
||||
double initialMargin;
|
||||
double direction;
|
||||
if (initMarginLegs.Any())
|
||||
{
|
||||
var netReceive = initMarginLegs.Sum(x => x.InterestDirection == (int)SwapDirectionEnum.收取 ? x.InterestPrincipalFix : -x.InterestPrincipalFix);
|
||||
initialMargin = Math.Abs((double)netReceive);
|
||||
direction = netReceive >= 0 ? 1 : -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
initialMargin = tradeInitialMargin ?? 0;
|
||||
direction = 1;
|
||||
}
|
||||
|
||||
if (isInitialCalc)
|
||||
{
|
||||
return CalcMaintenanceMargin(initialMargin, 0, direction);
|
||||
}
|
||||
|
||||
var additional = 0.0;
|
||||
//期初价比基:债券/指数/ETF统一为期初净价(PosiNetNoFeePrice,确认书"参考标的期初净价"同源),缺省回落期初全价
|
||||
var initNetPrice = (double)(underlyingLeg.PosiNetNoFeePrice ?? 0m);
|
||||
if (initNetPrice <= 0)
|
||||
{
|
||||
initNetPrice = (double)underlyingLeg.PosiGrossPrice;
|
||||
}
|
||||
if (closePrice > 0 && initNetPrice > 0)
|
||||
{
|
||||
var tiers = isCustomerLong ? spanCfg.LongSpans : spanCfg.ShortSpans;
|
||||
var tier = MatchTier(tiers, isCustomerLong, closePrice / initNetPrice);
|
||||
//追保金额基数:期初全价×券面总额(债券)/ 期初价格×名义份额(指数/ETF),同为 期初价×数量
|
||||
additional = CalcAdditionalMargin(tier, (double)underlyingLeg.PosiGrossPrice, (double)underlyingLeg.PosiQuantity);
|
||||
}
|
||||
|
||||
return CalcMaintenanceMargin(initialMargin, additional, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
/// <summary>
|
||||
/// 剩余可用授信 = 有效授信 − 已使用授信。
|
||||
/// 已使用授信 = 授信出入表 Σ(amount)(入金负/出金正)——入金使可用授信上升、出金使其收缩(业务口径)。
|
||||
/// 已使用授信 = 授信出入表 Σ(amount)(占用正/释放负,BUG-01 修正口径)——占用使可用授信收缩、释放使其回升。
|
||||
/// </summary>
|
||||
public double GetAvailableCredit(int clientId, DateTime valueDate)
|
||||
{
|
||||
@@ -54,16 +54,16 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。
|
||||
/// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿——
|
||||
/// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。
|
||||
/// 授信腿只写授信出入表占用(入金方向记负数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 授信腿只写授信出入表占用(占用记正数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。
|
||||
/// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。
|
||||
/// </summary>
|
||||
public void ApplyMarginFundTags(trade td, List<swap_position> marginLegs, ClientCashInCashOutService cashService, bool ignoreMoneyCheck)
|
||||
{
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
//重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写
|
||||
//(阶段四追加保证金占用落地后需区分保留追加部分)
|
||||
creditService.RemoveByTrade(td.id);
|
||||
//重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写;
|
||||
//追加保证金占用(阶段四 EOD 写入)不随重写清除——其资金记录不在本方法删除范围,由 EOD 幂等维护
|
||||
creditService.RemoveByTrade(td.id, keepAdditionalMargin: true);
|
||||
|
||||
var valueDate = td.TradeDate ?? DateTime.Now;
|
||||
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
|
||||
@@ -111,8 +111,8 @@ namespace YLErp.Modules.SwapModule
|
||||
if (plan != null && plan.CreditAmount > 0)
|
||||
{
|
||||
//整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。
|
||||
//入金方向记负数(业务口径:出入表金额与资金流水同号,入金负/出金正;入金使可用授信上升)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, -plan.CreditAmount, happenDate,
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用");
|
||||
}
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,现金部分按差额产生
|
||||
@@ -136,13 +136,15 @@ namespace YLErp.Modules.SwapModule
|
||||
var leg = plan.Leg;
|
||||
//应付额 = 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;
|
||||
|
||||
var creditLeg = leg.Clone();
|
||||
creditLeg.id = 0;
|
||||
creditLeg.PositionId = 0;
|
||||
creditLeg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
//授信腿倒挤 = 原 fix − 现金 fix(BUG-20:两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
creditLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
creditLeg.FundTag = ConsFundTag.Credit;
|
||||
creditLeg.OptId = UserId;
|
||||
creditLeg.OptName = UserName;
|
||||
|
||||
@@ -1514,8 +1514,9 @@ namespace YLErp.Modules.SwapModule
|
||||
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId);
|
||||
var eodSwaps = DbContext.eod_swap.Where(x => x.SwapTradeId == tradeId);
|
||||
var clientcashinouts = DbContext.ClientCashInCashOut.Where(x => x.TradeId == tradeId && x.Action == ClientCashInCashOut.系统操作_应付预付金);
|
||||
// R4:修改清除资金记录时同步清理授信出入记录,重确认时按最新标签重写
|
||||
new ClientCreditInoutService(this).RemoveByTrade(tradeId);
|
||||
// R4:修改清除 应付预付金 资金记录时同步清理对应授信出入记录,重确认时按最新标签重写;
|
||||
// 追加保证金资金记录(阶段四 EOD 产生)不在本方法删除范围,其授信占用一并保留
|
||||
new ClientCreditInoutService(this).RemoveByTrade(tradeId, keepAdditionalMargin: true);
|
||||
swapFlowEvents.ForEach(x =>
|
||||
{
|
||||
x.DataState = (int)SwapFlowDateStateEnum.废弃;
|
||||
|
||||
Reference in New Issue
Block a user