#EQD-7247 客户维度:维持保证金计算不正确
This commit is contained in:
@@ -13,8 +13,8 @@ namespace YLErp.Modules.MarginModule
|
||||
/// <summary>
|
||||
/// 预付金模板V2 取数帮助类:按交易绑定的模板(trade_margin_template → margin_template_v2 → margin_template_detail)
|
||||
/// 取 初始预付金率x / 维持预付金率y,供交易确认书、预付金计算等消费点统一使用。
|
||||
/// 匹配维度:簿记账户范围(BookIds,空=全部)→ 生效日期(ValueDate 最新)→
|
||||
/// 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
|
||||
/// 匹配维度:簿记账户范围(BookIds 显式限定且匹配者优先于空=全部的通配,同层多候选 2026-08-28 裁定)→
|
||||
/// 生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
|
||||
/// </summary>
|
||||
public static class MarginTemplateV2RateHelper
|
||||
{
|
||||
@@ -214,6 +214,7 @@ namespace YLErp.Modules.MarginModule
|
||||
/// 客户等级经 client.LevelId → clientlevel.LevelName 解析;ValueDate 最新 ≤ 业务日),
|
||||
/// 模板须有效、适用结构含收益互换且适用当前交易簿记账户;
|
||||
/// 3.全局默认 margin_template_v2(IsDefault && !IsForClient && IsValid && 适用收益互换,且适用当前簿记账户,ValueDate 最新)。
|
||||
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
|
||||
/// </summary>
|
||||
public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db)
|
||||
{
|
||||
@@ -268,25 +269,25 @@ namespace YLErp.Modules.MarginModule
|
||||
&& mt.IsValid && mt.TradeTypes.Contains("收益互换")
|
||||
orderby cmt.ValueDate descending, cmt.id descending
|
||||
select mt).ToList();
|
||||
|
||||
var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
|
||||
|
||||
//同层多候选:簿记条件满足者优先,其次绑定生效日最新(2026-08-28 裁定)
|
||||
var ret = PickByBookScopePreference(clientTemplate, tradeAssetId, out var clientByBook);
|
||||
if (ret != null)
|
||||
{
|
||||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{ret.id}");
|
||||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{ret.id}{(clientByBook ? "(簿记账户匹配优先)" : "")}");
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
//3.全局默认
|
||||
var globalDefault = db.margin_template_v2.AsNoTracking()
|
||||
//3.全局默认(同层多候选:簿记条件满足者优先,其次模板生效日最新——双全局默认并存时通配模板不再凭 id 最新恒胜,2026-08-28)
|
||||
var globalDefault = PickByBookScopePreference(db.margin_template_v2.AsNoTracking()
|
||||
.Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.ThenByDescending(x => x.id)
|
||||
.ToList()
|
||||
.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
|
||||
.ToList(), tradeAssetId, out var globalByBook);
|
||||
if (globalDefault != null)
|
||||
{
|
||||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}");
|
||||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "(簿记账户匹配优先)" : "")}");
|
||||
}
|
||||
return globalDefault;
|
||||
}
|
||||
@@ -298,6 +299,7 @@ namespace YLErp.Modules.MarginModule
|
||||
/// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退);
|
||||
/// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效、适用结构含收益互换且适用当前簿记账户,先过滤有效再取绑定最新);
|
||||
/// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,按每笔交易簿记账户取ValueDate/id 最新)。
|
||||
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
|
||||
/// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。
|
||||
/// </summary>
|
||||
public static Dictionary<int, margin_template_v2> ResolveTieredTemplates(List<trade> trades, DateTime valueDate, YLContext db)
|
||||
@@ -400,20 +402,23 @@ namespace YLErp.Modules.MarginModule
|
||||
continue;
|
||||
}
|
||||
var levelName = levelNames.TryGetValue(trade.ClientId, out var clientLevelName) ? clientLevelName : null;
|
||||
var clientTemplate = clientTemplateCandidates.FirstOrDefault(x =>
|
||||
(x.ClientId == trade.ClientId || (x.ClientId == 0 && x.ClientLevel == levelName))
|
||||
&& x.Template.IsApplicableToBook(trade.AssetId));
|
||||
//同层多候选:先按客户/等级筛出候选(保持绑定生效日降序),再簿记条件满足者优先、其次生效日最新(与单笔解析一致)
|
||||
var clientMatchCandidates = clientTemplateCandidates
|
||||
.Where(x => x.ClientId == trade.ClientId || (x.ClientId == 0 && x.ClientLevel == levelName))
|
||||
.Select(x => x.Template)
|
||||
.ToList();
|
||||
var clientTemplate = PickByBookScopePreference(clientMatchCandidates, trade.AssetId, out var clientByBook);
|
||||
if (clientTemplate != null)
|
||||
{
|
||||
result[trade.id] = clientTemplate.Template;
|
||||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{clientTemplate.Template.id}");
|
||||
result[trade.id] = clientTemplate;
|
||||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{clientTemplate.id}{(clientByBook ? "(簿记账户匹配优先)" : "")}");
|
||||
continue;
|
||||
}
|
||||
var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId));
|
||||
var globalDefault = PickByBookScopePreference(globalDefaults, trade.AssetId, out var globalByBook);
|
||||
if (globalDefault != null)
|
||||
{
|
||||
result[trade.id] = globalDefault;
|
||||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}");
|
||||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "(簿记账户匹配优先)" : "")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -424,6 +429,35 @@ namespace YLErp.Modules.MarginModule
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同层级多候选并存时的取舍(2026-08-28 裁定,纯函数单测覆盖):
|
||||
/// 入参候选序须已按生效日最新在前(客户层=绑定生效日,全局层=模板生效日,同日按 id 最新)。
|
||||
/// 1.存在"簿记条件满足"的候选(BookIds 显式限定且经 IsApplicableToBook 命中当前交易簿记账户)→ 优先取之,
|
||||
/// 多个命中时仍按传入顺序取生效日最新;
|
||||
/// 2.否则取顺序第一个(生效日最新)。
|
||||
/// 背景:双全局默认并存(如 无预付金限定簿记 + 区间追保通配)时,原"先按生效日/id 取最新再验簿记"
|
||||
/// 使通配模板恒胜、限定簿记模板永不生效(交易2572/2573 实证)。
|
||||
/// preferredByBookScope=true 表示本次由簿记匹配压过了顺序更前的通配候选(供日志辨识)。
|
||||
/// </summary>
|
||||
public static margin_template_v2 PickByBookScopePreference(List<margin_template_v2> candidatesByLatestFirst, int? tradeAssetId, out bool preferredByBookScope)
|
||||
{
|
||||
preferredByBookScope = false;
|
||||
if (candidatesByLatestFirst == null || candidatesByLatestFirst.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var applicable = candidatesByLatestFirst.Where(x => x.IsApplicableToBook(tradeAssetId)).ToList();
|
||||
var bookMatched = applicable.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.BookIds));
|
||||
if (bookMatched == null)
|
||||
{
|
||||
return applicable.FirstOrDefault();
|
||||
}
|
||||
|
||||
preferredByBookScope = applicable[0].id != bookMatched.id;
|
||||
return bookMatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标的细分分类判定钩子(ETF 子类,2026-08-24 实装):基金类标的返回标的维护页维护的
|
||||
/// underlying_manager.EtfSubType(取值来自数据字典"ETF 子类":国债 ETF/政金债 ETF/…/科创债 ETF/可转债 ETF,
|
||||
|
||||
Reference in New Issue
Block a user