Files
zszq-trs/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceQueryService.cs
T

188 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Helpers;
using YLErp.Modules.MarginModule;
namespace YLErp.Modules.SwapModule.Margin
{
/// <summary>
/// R2 阶段三 §3.2 估值报告/可用资金查询输入(静态查询,供 ClientBalanceUtility 与 RealTimeClientBanlanceService 共用,保证三处口径一致)。
/// 口径:
/// - 互换初始保证金(净收取为正)= 客户 应付预付金 流水收付净额取反 + 初始预付金授信占用净额
/// (授信垫付的初始预付金无资金流水,2026-08-27 补入口径,否则授信垫付客户初始保证金展示为 0);
/// - 交易维度追加保证金(合约维度)= Σ(维持保证金 − 累计保证金):
/// 维持保证金取当日 trade_span.Spv(区间追保结构引擎产出,我方净收取为正);
/// 累计保证金 = 该交易 应付预付金+追加保证金 流水收付净额 + 追加保证金授信占用净额(§0 口径,阶段四 §4.1 起)
/// + 初始预付金授信占用净额(非"追加保证金"前缀,授信垫付的初始预付金无现金流水,2026-08-27 与 EOD 追保侧同步修正);
/// 仅统计规则15(区间追保结构,R1 三层级解析,与引擎同口径)且有当日 trade_span 的交易。
/// </summary>
public static class SwapSpanBalanceQueryService
{
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金缺口查询");
/// <summary>
/// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。
/// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)——
/// 授信垫付的初始预付金不产生资金流水,只算流水会把授信垫付部分漏掉(展示为 0)。
/// </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();
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;
}
/// <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();
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<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);
//初始预付金的授信占用净额(非"追加保证金"前缀:簿记初始占用 + 平仓释放取负,Σ(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;
}
/// <summary>
/// 合约维度追保金额输入(2026-08-28 新口径):当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(按客户)。
/// 仅统计 Action=系统操作_追加保证金 且 HappenDate=当日 的现金流水(−Σ(Money),收取为正)——
/// 授信部分只写 client_credit_inout 不产生资金记录(授信不追、现金才追);
/// 腿化改造后现金流水逐日增量(每日新腿一条、HappenDate=结算日,同日重跑先清后建,幂等);
/// 该 Action 仅 EOD 追保会写(手工链路写应付预付金,语义天然隔离);追保回落不返还(无负记录),结果天然 ≥ 0。
/// 注意:EOD 未跑(日间实时)时当日无记录,结果为 0——与通知书口径一致。
/// </summary>
public static Dictionary<int, double> GetTodayCashAdditionalMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
{
if (clientIds == null || clientIds.Count == 0)
{
return new Dictionary<int, double>();
}
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);
}
}
}