using YLErp.BLL; using YLErp.DBModels; using YLErp.Enums; using YLErp.Modules.MarginModule; using YLErp.Modules.TradeModule; namespace YLErp.Modules.SwapModule.Margin { /// /// 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 占用口径),不在本服务范围。 /// public class SwapAdditionalMarginService : YLBaseService { public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo) { } public SwapAdditionalMarginService(YLBaseService baseService) : base(baseService) { } /// /// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。 /// public void SettleAdditionalMargin(DateTime settleDate, List 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(); 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); } } } } } }