using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Modules.MarginModule;
using YLErp.Modules.TradeModule;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule.Margin
{
///
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"。
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 已缴保证金净额
/// (应付预付金现金净收额 + 初始授信占用净额——
/// 2026-08-27 修正:授信垫付的初始预付金不产生应付预付金流水,此前未计入已缴导致每个结算日按维持全额重复开追加);
/// 需求上升只增不减、回落不返还。
/// 腿化改造(2026-08-27):增量>0 时生成一条与手工追加预付金同形态的交易腿(InterestMode=追加预付金、
/// OptName="EOD追保" 打标),并复用手工确认链路的 SwapFundTagService.ApplyMarginFundTags 完成授信/现金簿记
/// (资金来源走腿 fund_tag→交易级 margin_fund_source 回退链;授信部分写授信出入表占用绑腿、不产生流水;
/// 现金部分出 Action=系统操作-追加保证金 流水、Deal=现金腿 id——Action 与手工腿簿记的"应付预付金"区隔,
/// 保住 EQD-6952 资金通知书聚合口径);平仓返还(ReleaseMarginByTag)与交易回退清理由既有链路自动获得。
/// 幂等:重跑先清(本结算日起 EOD 旧追保腿及其簿记)后建;eod_swap_position 当日行显式补写(方案A——
/// 腿生成晚于当日 SwapPositionCompose,快照须补齐,见 SwapEodPositionService.SaveEodAdditionalMarginPosition)。
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算"。
/// 客户维度(MarginWatchRule=1/NULL)不产生资金记录(§0 占用口径),不在本服务范围。
///
public class SwapAdditionalMarginService : YLBaseService
{
private static readonly IYcLogger logger = LogFactory.GetLogger("EOD追保");
///
/// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分,
/// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。
///
public const string EodOptName = "EOD追保";
public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo)
{
}
public SwapAdditionalMarginService(YLBaseService baseService) : base(baseService)
{
}
///
/// EOD 追保腿识别(InterestMode=追加预付金 + OptName 打标);
/// happenDateFrom 限定重跑/回退窗口起点,防误删更早历史日已归属腿。
///
public static bool IsEodMarginLeg(swap_position leg, DateTime? happenDateFrom = null)
{
return leg != null
&& leg.InterestMode == (int)InterestModeEnum.追加预付金
&& leg.OptName == EodOptName
&& (happenDateFrom == null || leg.HappenDate >= happenDateFrom);
}
///
/// 构造 EOD 追保腿(纯函数,便于单测;落库与 PosiNumber 回填由调用方完成):
/// 与手工追加预付金腿同形态(PositionType=0 资金腿、PosiDirection=0、IsInitial、InterestMode=追加预付金、
/// 收取方向、fix=增量两位舍入),FundTag=null 回退交易级资金来源,OptName 打标 EOD 追保。
///
public static swap_position BuildEodMarginLeg(trade td, double increment, DateTime settleDate, int optId)
{
return new swap_position
{
SwapTradeId = td.id,
PositionType = 0,
PosiDirection = 0,
IsInitial = true,
InterestMode = (int)InterestModeEnum.追加预付金,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestPrincipalFix = Math.Round(Convert.ToDecimal(increment), 2, MidpointRounding.AwayFromZero),
HappenDate = settleDate,
PosiStartDate = td.StartDate ?? settleDate,
PosiMatuirityDate = td.ExerciseDate,
Currency = td.SettlementCurrency ?? ConsGlobal.Currency.CNY,
FundTag = null,
OptId = optId,
OptName = EodOptName,
OptTime = DateTime.Now
};
}
///
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
/// 顶层兜异常日志(Error 级)后原样抛出,避免异常栈被 EOD 框架层吞掉无从定位。
///
public void SettleAdditionalMargin(DateTime settleDate, List clientFilter = null)
{
try
{
SettleAdditionalMarginCore(settleDate, clientFilter);
}
catch (Exception ex)
{
var filterDesc = clientFilter != null && clientFilter.Any() ? string.Join(",", clientFilter) : "全部";
logger.Error($"EOD追保:结算日{settleDate:yyyy-MM-dd}(客户范围:{filterDesc})追保腿生成异常", ex);
throw;
}
}
private void SettleAdditionalMarginCore(DateTime settleDate, List clientFilter)
{
//合约维度盯市客户
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)
{
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 无合约维度盯市(MarginWatchRule=0)客户,结束");
return;
}
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 合约维度盯市客户{watchClientIds.Count}个({string.Join(",", watchClientIds)})");
//存续中的互换交易(状态口径与 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)
{
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 盯市客户名下无存续收益互换交易,结束");
return;
}
//开始日门槛:未到交易开始日(未起息)的不参与追保——维持保证金引擎从成交日就产出 trade_span,
//初始预付金流水却到开始日才入账,不过滤会在 成交日~开始日 之间把未到期的初始预付金
//误判成缺口、按维持全额追加(2026-08-28 交易2571实证:07-31成交/08-03起息,重刷07-31误追500000)
var notStartedTrades = trades.Where(t => (t.StartDate ?? t.TradeDate) > settleDate).ToList();
if (notStartedTrades.Count > 0)
{
logger.Info($"EOD追保:{notStartedTrades.Count}笔交易未到开始日(id=[{string.Join(",", notStartedTrades.Select(t => t.id))}]),不参与追保结算");
trades = trades.Except(notStartedTrades).ToList();
if (trades.Count == 0)
{
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 交易均未到开始日,结束");
return;
}
}
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)——
//交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算
var candidateCount = trades.Count;
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext);
trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构).ToList();
if (candidateCount > trades.Count)
{
logger.Info($"EOD追保:{candidateCount - trades.Count}笔交易模板非区间追保结构(或三级未命中),不参与追保结算");
}
var tradeIds = trades.Select(t => t.id).ToList();
if (tradeIds.Count == 0)
{
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 规则15交易为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;口径与 EOD canonical 一致:非作废+已确认/已结算。
//腿化改造后现金流水 Deal=现金腿 id(原 Deal==0 条件去除):该 Action 只有 EOD 会写,手工链路写应付预付金,语义天然隔离)
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.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);
//初始预付金的授信占用净额(非"追加保证金"前缀:簿记初始占用 + 平仓释放取负,Σ(amount) 即净已缴):
//授信腿不产生应付预付金流水,目标追加里只扣现金净收额会把授信垫付的初始预付金漏掉——
//每个结算日都按维持保证金全额重复开追加(BUG:多收授信占用/现金,2026-08-27 交易2538实证:初始授信200万未扣、首日全额追加280.8万)
var initCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
.Where(x => x.trade_id != null && tradeIds.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, Funded = g.Sum(x => x.amount) })
.ToDictionary(x => x.TradeId, x => x.Funded);
var fundTagService = new SwapFundTagService(this);
var cashService = new ClientCashInCashOutService(this);
var eodPositionService = new SwapEodPositionService(this);
var flowEventService = new SwapFlowEventService(this);
var settledTrades = 0;
var settledLegs = 0;
var totalIncrement = 0d;
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)
{
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——当日无trade_span维持保证金或维持≤0(maintenance={maintenance:0.00})");
continue;
}
//目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额,
//授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加)
var payable = payableNetByTrade.TryGetValue(td.id, out var p) ? p : 0;
var initCredit = initCreditByTrade.TryGetValue(td.id, out var ic) ? ic : 0;
var payableNet = payable + initCredit;
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet);
if (target <= 0)
{
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——目标追加≤0:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00})已覆盖");
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)
{
//已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——增量≤0:目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),追保回落不返还");
continue;
}
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})需追加:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00}),目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),本次增量={increment:0.00}");
//幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建;
//手工追加预付金腿(OptName≠EOD追保)不受影响
CleanupEodMarginLegs(td, settleDate);
//建腿:与手工追加预付金同形态,FundTag=null 回退交易级资金来源
var legIdsBefore = DbContext.swap_position.Where(x => x.SwapTradeId == td.id).Select(x => x.id).ToList();
var leg = BuildEodMarginLeg(td, increment, settleDate, UserId);
DbContext.swap_position.Add(leg);
DbContext.SaveChanges();
leg.PosiNumber = $"{td.TradeNumber}-{leg.id}";
//簿记:复用手工确认链路的标签分配(额度内授信占账、跨界拆单、现金出流水),
//Action 独立为 追加保证金(EQD-6952 口径)、占用 remark 带"追加保证金"前缀(累计/清理链路硬性识别点);
//resetExistingBookings=false——本交易初始预付金占用不在此重写(EOD 只簿记本次新腿,旧追保簿记上方已清)
fundTagService.ApplyMarginFundTags(td, new List { leg }, cashService, ignoreMoneyCheck: false,
cashAction: ClientCashInCashOut.系统操作_追加保证金,
occupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "占用",
splitOccupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "拆单授信部分",
bookingDate: settleDate,
resetExistingBookings: false);
//本次新腿 = 追保腿 + 可能的拆单现金腿(SplitLeg 落库时打了服务身份 OptName,回打 EOD 标识保证幂等清理覆盖完整)
var newLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && x.IsInitial
&& x.InterestMode == (int)InterestModeEnum.追加预付金 && !legIdsBefore.Contains(x.id))
.ToList();
newLegs.ForEach(x => x.OptName = EodOptName);
DbContext.SaveChanges();
logger.Info($"EOD追保:交易{td.id} 追保腿落库完成——新腿{newLegs.Count}条(id=[{string.Join(",", newLegs.Select(x => x.id))}],含拆单现金腿)");
settledTrades++;
settledLegs += newLegs.Count;
totalIncrement += increment;
//实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿——
//整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用);
//克隆继承定稿标签,供平仓返还分流与后续快照链使用
foreach (var newLeg in newLegs)
{
var clone = newLeg.Clone();
clone.id = 0;
clone.PositionId = newLeg.id;
clone.IsInitial = false;
DbContext.swap_position.Add(clone);
}
DbContext.SaveChanges();
flowEventService.InitEvent(newLegs, td, EodOptName);
DbContext.SaveChanges();
//方案A:腿生成晚于当日 SwapPositionCompose(当日 trade_span 结算后才可算增量),显式补写当日 eod_swap_position 行,
//使当日报表明细(PostionMarginGain 等)不漏计;下一结算日快照由 Compose 先清后建正常接管
foreach (var newLeg in newLegs)
{
eodPositionService.SaveEodAdditionalMarginPosition(td, newLeg, settleDate);
}
}
}
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 完成——规则15交易{tradeIds.Count}笔,{settledTrades}笔产生追保(腿{settledLegs}条,合计增量{totalIncrement:0.00})");
}
///
/// 幂等清理:删除本交易 本结算日起 EOD 生成的旧追保腿(含其实时克隆)及其簿记——
/// 现金流水(Action=追加保证金,Deal=腿id,按现行 DeleteTradeCashInCashOut 习惯硬删)、
/// 授信占用(position_id=腿id)、开仓事件、方案A 补写的当日及以后 eod_swap_position 行。
///
private void CleanupEodMarginLegs(trade td, DateTime settleDate)
{
var oldLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id
&& x.InterestMode == (int)InterestModeEnum.追加预付金
&& x.OptName == EodOptName
&& x.HappenDate >= settleDate)
.ToList();
if (oldLegs.Count == 0)
{
return;
}
var oldLegIds = oldLegs.Select(x => x.id).ToList();
//簿记(流水 Deal/占用 position_id/快照 PositionId/事件 PositionId)均绑期初腿(拆单现金腿也是 IsInitial 的独立腿)
var oldInitialIds = oldLegs.Where(x => x.IsInitial).Select(x => x.id).ToList();
var oldCashRecords = DbContext.ClientCashInCashOut.Where(x => x.TradeId == td.id
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
&& oldInitialIds.Contains(x.Deal))
.ToList();
DbContext.ClientCashInCashOut.RemoveRange(oldCashRecords);
var oldCreditRecords = DbContext.client_credit_inout.Where(x => x.trade_id == td.id
&& x.position_id != null && oldInitialIds.Contains(x.position_id.Value))
.ToList();
DbContext.client_credit_inout.RemoveRange(oldCreditRecords);
var oldEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == td.id
&& x.EventType == (int)SwapFlowEventTypeEnum.开仓 && oldInitialIds.Contains(x.PositionId))
.ToList();
DbContext.swap_flow_event.RemoveRange(oldEvents);
var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id
&& x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId))
.ToList();
logger.Info($"EOD追保:交易{td.id} 幂等清理(重跑)——删EOD追保腿{oldLegs.Count}条(id=[{string.Join(",", oldLegs.Select(x => x.id))}])及其簿记:现金流水{oldCashRecords.Count}条、授信占用{oldCreditRecords.Count}条、开仓事件{oldEvents.Count}条、eod快照{oldEodRows.Count}行");
DbContext.eod_swap_position.RemoveRange(oldEodRows);
DbContext.swap_position.RemoveRange(oldLegs);
DbContext.SaveChanges();
}
}
}