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;
}
///
/// 合约维度追保金额输入(2026-08-28 新口径):当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(按客户)。
/// 仅统计 Action=系统操作_追加保证金 且 HappenDate=当日 的现金流水(−Σ(Money),收取为正)——
/// 授信部分只写 client_credit_inout 不产生资金记录(授信不追、现金才追);
/// 腿化改造后现金流水逐日增量(每日新腿一条、HappenDate=结算日,同日重跑先清后建,幂等);
/// 该 Action 仅 EOD 追保会写(手工链路写应付预付金,语义天然隔离);追保回落不返还(无负记录),结果天然 ≥ 0。
/// 注意:EOD 未跑(日间实时)时当日无记录,结果为 0——与通知书口径一致。
///
public static Dictionary GetTodayCashAdditionalMarginByClients(List clientIds, DateTime valueDate, YLContext db)
{
if (clientIds == null || clientIds.Count == 0)
{
return new Dictionary();
}
var dayStart = valueDate.Date;
var dayEnd = dayStart.AddDays(1);
return db.ClientCashInCashOut.AsNoTracking()
.Where(x => clientIds.Contains(x.ClientId ?? 0)
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
&& x.HappenDate >= dayStart && x.HappenDate < dayEnd
&& x.ValidState != ConsGlobal.InValid
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
&& x.Money != null)
.GroupBy(x => x.ClientId ?? 0)
.Select(g => new { ClientId = g.Key, Sum = -g.Sum(x => x.Money ?? 0d) })
.ToDictionary(x => x.ClientId, x => x.Sum);
}
}
}