refactor(swap): EQD-6968 取价口径自洽化——排除日纯跳过+持仓延续重置日显式再定盘

取价规则只由交易属性决定(算不算尾/interest_rule/重置日历),平仓只切窗口:
① BuildSegmentRates 排除日(不算尾尾日)一概不取价(有价也不取)——事件利率恒为
   末段已消费利率,与金额同源、与平仓时刻无关(旧口径下午平仓落库新利率,金额
   却用旧利率,且上午/下午记录不同);金额零变更(排除日本就零消费)。
② GetFloatRate 不算尾重置日不再先试取尾日定盘(原'有价则取'分支删除)。
③ 剩余持仓的新周期利率改由 SwapEodPositionService 显式获取:平仓日恰为重置日
   且剩余>0 时,快照 FloatRate 经 ResolveOngoingResetFixing 再定盘为当日新定盘
   (生产 GetFixingOrThrow,缺价抛——与 ByEod 重置日/EodCheckSettlePrice 同口径;
   原'有价则取'顺带承担此载体职责,现显式分离)。全平/算尾/观察日无需再定盘。
④ 无日终快照播种 priorValueDate 恒=开始日-1:首重置日(=开始日)的定盘覆盖
   [开始日,下一重置日)全部计息日(不算头时 7/7 起仍属首段),必须落在取价窗内。
   原仅算头回拨一天,不算头时首段误用种子利率(靠②的尾日取价回填掩盖,现已根治)。
   '不算头少计一天'仍由计息边界 IncludeStart=false 承担,天数不变、利率归位。
FIX 日志同步:排除日不取价/EodCloseRefix 再定盘 全程可审计。

验证:全量 976 例 145 败与改动前基线失败名单 diff=0;内存套件(含
ContractReferenceOracle 金标准/InterestEodTailSnapshot)全绿
This commit is contained in:
hjhan
2026-08-19 11:06:52 +08:00
parent e779347488
commit fafae5cac0
2 changed files with 47 additions and 23 deletions
+24 -22
View File
@@ -850,8 +850,9 @@ namespace YLErp.Modules.SwapModule
/// EQD-6968 取价依赖三判定之一。锚点口径注记:GetFloatRate 传交易起始日 td.StartDate
/// 分段重放/当日归周期判定传 position.PosiStartDate——非初始持仓(部分平仓剩余仓)两锚点可能不同,
/// 本方法只收口公式、不统一锚点(统一属行为变更,需业务定调)。
/// SwapEodPositionService 的"持仓延续腿重置日再定盘"亦用本判定(td.StartDate 锚点)。
/// </summary>
private static bool IsResetDay(DateTime date, DateTime anchorDate, int period)
internal static bool IsResetDay(DateTime date, DateTime anchorDate, int period)
=> (date - anchorDate).Days % period == 0;
/// <summary>
@@ -879,6 +880,17 @@ namespace YLErp.Modules.SwapModule
return preEod.FloatRate;
}
// EQD-6968 口径自洽化:不算尾(calcLast=false)时 endDate 当天不计息,其定盘一概不取
// (有价也不取)——事件/回写利率与金额同源(末段已消费利率),杜绝"上午/下午落库利率不同"。
// 剩余持仓的新周期利率由 SwapEodPositionService 的"重置日再定盘"显式获取,不靠此处顺带。
if (!swap && !calcLast && isResetDay)
{
var keptNoFetch = preEod.id != 0 ? preEod.FloatRate : position.FloatRate;
SwapCalcTrace.Critical(
$"FIX GetFloatRate p{position.id} 不算尾重置日→不取尾日定盘,沿用已有利率={keptNoFetch:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})");
return keptNoFetch;
}
if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate))
{
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 重置日→取{rateDate:yyyy-MM-dd}定盘={rate:P6}");
@@ -1115,11 +1127,12 @@ namespace YLErp.Modules.SwapModule
preEod.FloatRate = floatRate;
preEod.TdInterestPrincipal = posiPrincipal;
preEod.PosiNotionalValue = posiPrincipal;
preEod.ValueDate = td.StartDate.Value;
if (calcFirst)
{
preEod.ValueDate = preEod.ValueDate.AddDays(-1);
}
// priorValueDate 恒取 开始日-1:首个重置日(=开始日)的定盘覆盖 [开始日,下一重置日) 全部计息日
//(不算头时 7/7 起的计息日仍属首段),必须落在取价窗内。原实现仅算头回拨一天,
// 不算头时 fetchAfter=开始日 会跳过首重置日取价、首段误用种子利率
//(历史上靠 GetFloatRate 尾日取价回填种子掩盖;EQD-6968 自洽化后暴露并根治)。
// "不算头少计一天"由计息边界 IncludeStart=false 承担,与此处无关。
preEod.ValueDate = td.StartDate.Value.AddDays(-1);
}
return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal,
@@ -1359,10 +1372,10 @@ namespace YLErp.Modules.SwapModule
{
var resetDate = startDate.AddDays(i);
bool needFetch = (fetchAfterDate == null || resetDate > fetchAfterDate.Value);
// 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息
// 从 relaxedFixingFromDate(缺省取 endDate)起的重置日:有价则取(满足重置日=平仓日用新利率),
// 缺价则跳过取价——currentFloat 保持不变(沿用上一重置日利率),不回退、不告警、不抛异常。
// 算尾(calcLast=true)时所有重置日定盘照常强制取价。
// 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息——
// 排除日(EQD-6968 自洽化)一概不取价(有价也不取),currentFloat 保持末段已消费利率:
// 事件/快照回写的浮动利率与金额同源、与平仓时刻无关。剩余持仓的新周期利率由
// SwapEodPositionService 的"重置日定盘"显式获取,不靠排除日顺带。算尾照常强制取价。
bool isExcludedEnd = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate);
if (needFetch && !isExcludedEnd)
{
@@ -1370,19 +1383,8 @@ namespace YLErp.Modules.SwapModule
}
else if (needFetch && isExcludedEnd)
{
// 有价则取新利率(重置日=平仓日时仍优先使用新利率);无价则跳过——该日利率不参与计息
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r))
{
currentFloat = r;
SwapCalcTrace.Critical(
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日有价→取新定盘(取价日={fixingDate:yyyy-MM-dd})={r:P6}");
}
else
{
SwapCalcTrace.Critical(
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日缺价→跳过取价,沿用={currentFloat:P6}(该日不计息)");
}
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日(不计息)→不取价,沿用末段={currentFloat:P6}");
}
rates.Add((resetDate, spread + currentFloat));
}
@@ -117,6 +117,14 @@ namespace YLErp.Modules.SwapModule
req.ClosePosiNotionalValue, req.ClosePercent, req.EventType, req.TdClose,
req.OrginPv, req.Add, settment: false, req.NewCalcLast, req.CloseList);
/// <summary>
/// 持仓延续腿重置日再定盘(EQD-6968 口径自洽化接缝)。
/// 生产:Fr007IndexFixer.GetFixingOrThrow——缺价抛异常,与 ByEod 重置日再定盘/EodCheckSettlePrice
/// 同口径(EOD 时点当日 FR007 已由收盘前检查把关);测试:override 注入受控定盘。
/// </summary>
protected virtual decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate)
=> Fr007IndexFixer.Instance.GetFixingOrThrow(valueDate, position.interest_rule, position.FloatRateUnderlyingCode);
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
/// <summary>查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典)</summary>
@@ -1361,6 +1369,20 @@ namespace YLErp.Modules.SwapModule
//持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值:
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。
CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0);
// ── 持仓延续腿重置日再定盘(EQD-6968 自洽化)──
// 排除日取价已收口为"纯跳过":事件利率=末段已消费利率。但剩余持仓自当日起进入新计息周期,
// 快照 FloatRate 是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的
// 利率载体——平仓日恰为重置日时必须显式取当日新定盘(与 ByEod 日增路径的重置日行为同构)。
// 全平(剩余=0)/算尾(事件利率已是新定盘)/观察日(autoSwap 恒1已含当日)无需再定盘。
if (!autoSwap && !calcLast && posiNotionalValue > 0m
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
&& SwapDealService.IsResetDay(valueDate, td.StartDate.Value, position.interest_rest_days ?? 1))
{
var ongoingFixing = ResolveOngoingResetFixing(position, valueDate);
SwapCalcTrace.Critical(
$"FIX EodCloseRefix p{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}");
newEodPayPosition.FloatRate = ongoingFixing;
}
//利息端估值用信息
// TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。
// 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。