using YLErp.BLL; using YLErp.DBModels; using YLErp.Enums; using YLErp.Helpers; using YLErp.Modules.MarginModule; namespace YLErp.Modules.SwapModule.Margin { /// /// R2 阶段三 §3.2 估值报告/可用资金查询输入(静态查询,供 ClientBalanceUtility 与 RealTimeClientBanlanceService 共用,保证三处口径一致)。 /// 口径: /// - 互换初始保证金(净收取为正)= 客户 应付预付金 流水收付净额取反 + 初始预付金授信占用净额 /// (授信垫付的初始预付金无资金流水,2026-08-27 补入口径,否则授信垫付客户初始保证金展示为 0); /// - 交易维度追加保证金(合约维度)= Σ(维持保证金 − 累计保证金): /// 维持保证金取当日 trade_span.Spv(区间追保结构引擎产出,我方净收取为正); /// 累计保证金 = 该交易 应付预付金+追加保证金 流水收付净额 + 追加保证金授信占用净额(§0 口径,阶段四 §4.1 起) /// + 初始预付金授信占用净额(非"追加保证金"前缀,授信垫付的初始预付金无现金流水,2026-08-27 与 EOD 追保侧同步修正); /// 仅统计规则15(区间追保结构,R1 三层级解析,与引擎同口径)且有当日 trade_span 的交易。 /// public static class SwapSpanBalanceQueryService { private static readonly IYcLogger logger = LogFactory.GetLogger("预付金缺口查询"); /// /// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。 /// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)—— /// 授信垫付的初始预付金不产生资金流水,只算流水会把授信垫付部分漏掉(展示为 0)。 /// public static Dictionary GetSwapInitMarginByClients(List clientIds, DateTime valueDate, YLContext db) { if (clientIds == null || clientIds.Count == 0) { return new Dictionary(); } 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(); var result = flows.GroupBy(x => x.ClientId) .ToDictionary(g => g.Key, g => -g.Sum(x => x.Money)); //初始预付金的授信占用净额(占用记正/释放记负,Σ(amount) 即净已缴;trade_id != null 排除人工调整类记录, //"追加保证金"前缀为 EOD 追保占用,不计入初始保证金) var initCredit = db.client_credit_inout.AsNoTracking() .Where(x => clientIds.Contains(x.client_id) && x.trade_id != null && x.happen_date <= valueDate && (x.remark == null || !x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark))) .GroupBy(x => x.client_id) .Select(g => new { ClientId = g.Key, Sum = g.Sum(x => x.amount) }) .ToList(); foreach (var item in initCredit) { result[item.ClientId] = (result.TryGetValue(item.ClientId, out var cash) ? cash : 0d) + item.Sum; } return result; } /// /// 合约维度输入:交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金),按客户汇总。 /// public static Dictionary GetTradeAdditionalMarginByClients(List clientIds, DateTime valueDate, YLContext db) { var result = new Dictionary(); 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(); var droppedCount = spanTradeIds.Count - rule15TradeIds.Count; if (droppedCount > 0) { logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} {droppedCount}笔有span交易模板非规则15(或三级未命中),不计入交易维度追加合计"); } if (maintenance.Count == 0) { return result; } //累计保证金:该交易 应付预付金+追加保证金 流水收付净额取反(收取为正); //追加保证金的授信占用部分不产生资金流水(阶段二口径),阶段四 §4.1 起一并计入—— //否则结算后缺口残留(=授信部分),与可用资金公式里的 −已使用授信 形成双扣 var marginActions = new List { 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); //初始预付金的授信占用净额(非"追加保证金"前缀:簿记初始占用 + 平仓释放取负,Σ(amount) 即净已缴)—— //授信垫付的初始预付金不产生应付预付金流水,不计入会把授信初始占用当作未缴缺口多扣可用资金 //(与 SwapAdditionalMarginService EOD 追保侧同笔修正,2026-08-27 交易2538实证:初始授信200万未扣) var initCreditOccupied = db.client_credit_inout.AsNoTracking() .Where(x => x.trade_id != null && spanTradeIds.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, 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) - (initCreditOccupied.TryGetValue(x.TradeId, out var initOccupied) ? initOccupied : 0d)); result[group.Key] = total; } logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} 规则15交易{rule15TradeIds.Count}笔,客户{result.Count}个,交易维度追加合计:{string.Join(";", result.Select(kv => $"客户{kv.Key}={kv.Value:0.00}"))}"); return result; } } }