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);
}
}
}
}
}
}