feat: 保证金链路补全排查日志(Info为主,仅追保顶层异常用Error)

- EOD追保生成主流程:入口汇总/六处跳过原因/决策中间量(维持·已缴·目标·已补足·增量)/建腿落库/幂等清理/收尾汇总,顶层异常Error留栈
- 预付金模板取数:三级层级命中来源(交易绑定/客户默认/全局默认)+明细行命中(品种·期限档·x/y),null返回各留一句
- 规则15引擎:成功路径补收盘价→维持保证金,与既有无价格告警闭环
- EOD/实时客户资金:每客户一行维度分流结果(MarginWatchRule·可用资金·追保金额·可取资金及全部输入)
- 预付金缺口查询:非规则15剔除数与按客户追加合计;预付金簿记:授信/现金拆分结果
This commit is contained in:
hjhan
2026-08-28 12:08:27 +08:00
parent e52f5bfeb5
commit 03629a6dcd
7 changed files with 95 additions and 6 deletions
@@ -3,6 +3,7 @@ using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Modules.MarginModule;
using YLErp.Modules.TradeModule;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule.Margin
{
@@ -24,6 +25,8 @@ namespace YLErp.Modules.SwapModule.Margin
/// </summary>
public class SwapAdditionalMarginService : YLBaseService
{
private static readonly IYcLogger logger = LogFactory.GetLogger("EOD追保");
/// <summary>
/// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分,
/// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。
@@ -79,8 +82,23 @@ namespace YLErp.Modules.SwapModule.Margin
/// <summary>
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
/// 顶层兜异常日志(Error 级)后原样抛出,避免异常栈被 EOD 框架层吞掉无从定位。
/// </summary>
public void SettleAdditionalMargin(DateTime settleDate, List<int> 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<int> clientFilter)
{
//合约维度盯市客户
var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking()
@@ -93,8 +111,10 @@ namespace YLErp.Modules.SwapModule.Margin
}
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;
@@ -107,17 +127,24 @@ namespace YLErp.Modules.SwapModule.Margin
.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;
}
@@ -175,21 +202,28 @@ namespace YLErp.Modules.SwapModule.Margin
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维持保证金或维持≤0maintenance={maintenance:0.00}");
continue;
}
//目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额,
//授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加)
var payableNet = (payableNetByTrade.TryGetValue(td.id, out var payable) ? payable : 0)
+ (initCreditByTrade.TryGetValue(td.id, out var initCredit) ? initCredit : 0);
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;
@@ -198,8 +232,10 @@ namespace YLErp.Modules.SwapModule.Margin
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追保)不受影响
@@ -228,6 +264,10 @@ namespace YLErp.Modules.SwapModule.Margin
.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 场景不可用);
@@ -252,6 +292,8 @@ namespace YLErp.Modules.SwapModule.Margin
}
}
}
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 完成——规则15交易{tradeIds.Count}笔,{settledTrades}笔产生追保(腿{settledLegs}条,合计增量{totalIncrement:0.00}");
}
/// <summary>
@@ -289,6 +331,7 @@ namespace YLErp.Modules.SwapModule.Margin
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();
@@ -1,6 +1,7 @@
using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Helpers;
using YLErp.Modules.MarginModule;
namespace YLErp.Modules.SwapModule.Margin
@@ -18,6 +19,8 @@ namespace YLErp.Modules.SwapModule.Margin
/// </summary>
public static class SwapSpanBalanceQueryService
{
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金缺口查询");
/// <summary>
/// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。
/// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)——
@@ -95,6 +98,11 @@ namespace YLErp.Modules.SwapModule.Margin
.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;
@@ -142,6 +150,8 @@ namespace YLErp.Modules.SwapModule.Margin
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;
}
}
@@ -2,6 +2,7 @@ using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.TradeModule;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule
{
@@ -15,6 +16,8 @@ namespace YLErp.Modules.SwapModule
/// </summary>
public class SwapFundTagService : YLBaseService
{
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金簿记");
public SwapFundTagService(OptUserInfo userInfo) : base(userInfo)
{
}
@@ -167,6 +170,7 @@ namespace YLErp.Modules.SwapModule
.ThenBy(x => x.Leg.id)
.ToList();
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck);
logger.Info($"预付金簿记:交易{td.id}(客户{td.ClientId})簿记日{valueDate:yyyy-MM-dd}Action={cashAction})可用授信={creditAvailable:0.00}{marginLegs.Count}条预付金腿分配:{(plans.Any() ? string.Join("", plans.Select(p => $"{p.Leg.id}{p.Amount:0.00}{p.CreditAmount:0.00}/{p.CashAmount:0.00}{(p.NeedSplit ? "(拆单)" : "")}")) : "")}");
//先落库拆分的新现金腿(需要 id 才能绑定现金流水)
foreach (var plan in plans.Where(p => p.NeedSplit))