refactor(swap): EQD-6977 trace可读性第二批:①②与p{id}语义化——承接①→已并复利本金(capitalized)、②→段内已计利息(carryIn),日志/类头注/方法注/测试断言四处同步,运维凭日志即知两承接量含义;SwapDealService×12+SwapEodPositionService×1的p{position.id}统一改融资腿{position.id}(FIX GetFloatRate/Resolve/Segments/EodCloseRefix全系列);测试断言同步更新(此前漏查到一处trace断言已一并改);27/27罚息单测通过,Swap全套644例中仅2例DB依赖失败(干净树复验为既有环境问题)

This commit is contained in:
hjhan
2026-08-21 08:54:47 +08:00
parent 9d68e56797
commit 640c78b5a9
4 changed files with 43 additions and 43 deletions
@@ -157,40 +157,40 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
[TestMethod]
public void preEod复利段中兜底为零且账龄超重置周期_留退化告警trace()
{
// 场景:无日终快照 + 复利 + 段中平仓,事件 InterestPrincipal 仍是种子值(=平仓本金)→兜底=0。
// 账龄 25 天 ≥ 7 天重置周期:复利每周期并本理应>0=0 属退化——
// 场景:无日终快照 + 复利 + 段中平仓,事件 InterestPrincipal 仍是种子值(=平仓本金)→兜底已并复利本金=0。
// 账龄 25 天 ≥ 7 天重置周期:复利每周期并本理应>0,已并复利本金=0 属退化——
// 典型成因=interestWindowEmpty(当日已结息)早退未重放覆盖种子、或日终归档缺失。
var e = NormalEvent(settledAmount: 50_000m);
e.InterestPrincipal = Notional; // GetInterests 种子值:interestWindowEmpty 早退路径不会用重放基数覆盖它
var trace = new AccrualTrace();
RunMerge(Leg(InterestTypeEnum.), e, preEod: null, trace: trace);
StringAssert.Contains(trace.ToString(), "无preEod兜底=0",
"=0 且账龄超周期必须留告警,供事后核对日终归档/计息窗口根因");
StringAssert.Contains(trace.ToString(), "无preEod兜底已并复利本金=0",
"已并复利本金=0 且账龄超周期必须留告警,供事后核对日终归档/计息窗口根因");
}
[TestMethod]
public void preEod兜底为正_不留退化告警()
{
var e = NormalEvent(settledAmount: 50_000m);
e.InterestPrincipal = Notional + 100_000m; // 重放末次并本金后基数 → =100000 正常路径
e.InterestPrincipal = Notional + 100_000m; // 重放末次并本金后基数 → 已并复利本金=100000 正常路径
var trace = new AccrualTrace();
RunMerge(Leg(InterestTypeEnum.), e, preEod: null, trace: trace);
Assert.IsFalse(trace.ToString().Contains("兜底=0"), ">0 是正常兜底路径,不得告警");
Assert.IsFalse(trace.ToString().Contains("兜底已并复利本金=0"), "已并复利本金>0 是正常兜底路径,不得告警");
}
[TestMethod]
public void preEod真首日兜底为零_不留退化告警()
{
var p = Leg(InterestTypeEnum.);
p.PosiStartDate = UnwindDate; // 起息日当天平仓:账龄 0 < 重置周期,=0 是设计内约定(类头注)
p.PosiStartDate = UnwindDate; // 起息日当天平仓:账龄 0 < 重置周期,已并复利本金=0 是设计内约定(类头注)
var e = NormalEvent(settledAmount: 50_000m);
e.InterestPrincipal = Notional;
var trace = new AccrualTrace();
RunMerge(p, e, preEod: null, trace: trace);
Assert.IsFalse(trace.ToString().Contains("兜底=0"), "真首日 =0 合法,不得告警");
Assert.IsFalse(trace.ToString().Contains("兜底已并复利本金=0"), "真首日 已并复利本金=0 合法,不得告警");
}
[TestMethod]
@@ -12,9 +12,9 @@ namespace YLErp.Modules.SwapModule.Penalty;
/// getSpread(加点利差)/ getPreEod(上一日终快照行)/ tryGetFixing(定盘取价),本类零 DB 耦合、可 headless 单测。
///
/// 复利承接量(精确续接口径的关键)**必须取实际计息状态**,严禁冻结利率重放推导:
/// 承接① capitalized = max(0, preEod.TdInterestPrincipal×份额 closePrincipal) —— 实际滚动复利基数中已并入部分;
/// 承接② carryIn = 正常平仓流实结 InterestAmount —— 最近重置日后实际已计利息;
/// 无 preEod(首日平仓):①=0、②=实结金额。
/// 已并复利本金 capitalized = max(0, preEod.TdInterestPrincipal×份额 closePrincipal) —— 实际滚动复利基数中已并入部分;
/// 段内已计利息 carryIn = 正常平仓流实结 InterestAmount 已并复利本金 —— 最近重置日后实际已计利息;
/// 无 preEod(首日平仓):已并复利本金=0、段内已计利息=实结金额。
/// 逐腿全程 trace 落盘(SwapCalcTrace),供计算过程分析与错误定位。
/// </summary>
public static class PenaltyInterestFeeMerger
@@ -48,7 +48,7 @@ public static class PenaltyInterestFeeMerger
foreach (var position in fundingPositions)
{
// 正常平仓利息流(GetInterests 刚产出)——承接②的事实源与罚息并入目标
// 正常平仓利息流(GetInterests 刚产出)——段内已计利息的事实源与罚息并入目标
var normalEvent = interests.FirstOrDefault(x => x.PositionId == position.id);
if (normalEvent == null)
{
@@ -92,7 +92,7 @@ public static class PenaltyInterestFeeMerger
continue;
}
// 复利承接:实际滚动基数中已并入部分(+ 段内实际已计利息()。单利无并本金语义恒 0。
// 复利承接:实际滚动基数中已并入部分(已并复利本金+ 段内实际已计利息(段内已计利息)。单利无并本金语义恒 0。
var (capitalized, carryIn) = isCompound
? ResolveCompoundCarry(position, normalEvent, preEod, closePrincipal, share, unwindDate, trace)
: (0m, 0m);
@@ -115,18 +115,18 @@ public static class PenaltyInterestFeeMerger
$"窗口=[{unwindDate:yyyy-MM-dd}→{maturityDate:yyyy-MM-dd}] 平仓日已结={unwindDaySettled} 到期算尾={maturityCalcLast} | " +
$"本金 close={closePrincipal:F2} posi={r.PosiPrincipal:F2} share={share:P4} | " +
$"冻结利率={frozenRate.AllInRate:P6} 来源={rateSource} | " +
$"承接={capitalized:F4} ={carryIn:F4} 实结={normalEvent.InterestAmount:F4} | " +
$"承接[已并复利本金]={capitalized:F4} [段内已计利息]={carryIn:F4} 实结={normalEvent.InterestAmount:F4} | " +
$"罚息={penalty:F2} → InterestFee {feeBefore:F2}→{normalEvent.InterestFee:F2} PnL含罚息={normalEvent.InterestClosePnL:F2}");
}
}
/// <summary>
/// 复利承接量:实际滚动基数中已并入部分 + 最近重置日后实际已计利息(= 实结 )。
/// 复利承接量:已并复利本金(实际滚动基数中已并入部分+ 段内已计利息(最近重置日后实际已计= 实结 已并复利本金)。
///
/// 的取值依赖平仓日是否为重置日、有无日终快照(数据契约):
/// 段中平仓 + 有快照:TdInterestPrincipal 即当前段滚动基数(=本金+),直接作差;
/// 已并复利本金的取值依赖平仓日是否为重置日、有无日终快照(数据契约):
/// 段中平仓 + 有快照:TdInterestPrincipal 即当前段滚动基数(=本金+已并复利本金),直接作差;
/// 段中平仓 + 无快照:兜底取 normalEvent.InterestPrincipal——复利重放(CalcDailyCompoundInterest)
/// 会把它写为末次并本金后的基数(=被平份额本金+),同样是实际值而非推导值;
/// 会把它写为末次并本金后的基数(=被平份额本金+已并复利本金),同样是实际值而非推导值;
/// 重置日当天平仓:快照基数仍是【上一段】的(今日并入尚未发生),须改取
/// preEod.InterestIncomeSum(昨日全部待实现利息 = 今日并入新段基数的那部分)。
/// </summary>
@@ -143,41 +143,41 @@ public static class PenaltyInterestFeeMerger
capitalized = (preEod?.InterestIncomeSum ?? 0m) * share;
if (preEod != null)
trace?.Note(
$"PENALTY|融资腿{position.id} 承接推导 重置日平仓+有快照:快照{preEod.ValueDate:yyyy-MM-dd} " +
$"昨日待实现利息InterestIncomeSum={preEod.InterestIncomeSum:F4} ×share={share:P4} → ={capitalized:F4}");
$"PENALTY|融资腿{position.id} 承接推导(已并复利本金) 重置日平仓+有快照:快照{preEod.ValueDate:yyyy-MM-dd} " +
$"昨日待实现利息InterestIncomeSum={preEod.InterestIncomeSum:F4} ×share={share:P4} → 已并复利本金={capitalized:F4}");
if (preEod == null && (unwindDate - position.PosiStartDate).Days >= periodDays)
trace?.Note($"PENALTY|融资腿{position.id} 注意 无preEod且平仓日=重置日:退化0(此前重置并入额缺失,请核对日终归档完整性)");
trace?.Note($"PENALTY|融资腿{position.id} 注意 无preEod且平仓日=重置日:已并复利本金退化0(此前重置并入额缺失,请核对日终归档完整性)");
}
else if (preEod != null)
{
// 段中平仓+有快照(复利承接主路径): = 快照滚动基数×份额 − 平仓本金。全程留推导——
// 异常时凭此行即可区分"快照基数错 / share错 / 平仓本金错"三因,不必反推。
// 段中平仓+有快照(复利承接主路径):已并复利本金 = 快照滚动基数×份额 − 平仓本金。全程留推导——
// 结果异常时凭此行即可区分"快照基数错 / share错 / 平仓本金错"三因,不必反推。
var rawCarry = preEod.TdInterestPrincipal * share - closePrincipal;
capitalized = Math.Max(0m, rawCarry);
trace?.Note(
$"PENALTY|融资腿{position.id} 承接推导 段中平仓+有快照:快照{preEod.ValueDate:yyyy-MM-dd} " +
$"滚动基数TdInterestPrincipal={preEod.TdInterestPrincipal:F4} ×share={share:P4} −平仓本金{closePrincipal:F4} = {rawCarry:F4} → ={capitalized:F4}" +
$"PENALTY|融资腿{position.id} 承接推导(已并复利本金) 段中平仓+有快照:快照{preEod.ValueDate:yyyy-MM-dd} " +
$"滚动基数TdInterestPrincipal={preEod.TdInterestPrincipal:F4} ×share={share:P4} −平仓本金{closePrincipal:F4} = {rawCarry:F4} → 已并复利本金={capitalized:F4}" +
(rawCarry < 0m ? "(原始差为负已钳0:快照滚动基数×份额小于平仓本金,疑部分平仓比例与快照归档口径不一致,请核对eod_swap_position.TdInterestPrincipal" : ""));
}
else
{
capitalized = Math.Max(0m, normalEvent.InterestPrincipal - closePrincipal);
trace?.Note(
$"PENALTY|融资腿{position.id} 承接推导 段中平仓+无快照兜底:事件基数InterestPrincipal={normalEvent.InterestPrincipal:F4} −平仓本金{closePrincipal:F4} → ={capitalized:F4}");
// 兜底=0 但账龄已过重置周期:复利每周期并本,理应 >0——多为 interestWindowEmpty
$"PENALTY|融资腿{position.id} 承接推导(已并复利本金) 段中平仓+无快照兜底:事件基数InterestPrincipal={normalEvent.InterestPrincipal:F4} −平仓本金{closePrincipal:F4} → 已并复利本金={capitalized:F4}");
// 兜底已并复利本金=0 但账龄已过重置周期:复利每周期并本,理应 >0——多为 interestWindowEmpty
// (当日已结息)早退未重放覆盖种子值、或日终归档缺失。留痕含两侧基数与账龄,供直接定位根因。
var ageDays = (unwindDate - position.PosiStartDate).Days;
if (capitalized == 0m && ageDays >= periodDays)
trace?.Note(
$"PENALTY|融资腿{position.id} 注意 无preEod兜底=0但账龄{ageDays}天≥重置周期{periodDays}天:" +
$"PENALTY|融资腿{position.id} 注意 无preEod兜底已并复利本金=0但账龄{ageDays}天≥重置周期{periodDays}天:" +
$"事件基数{normalEvent.InterestPrincipal:F2}=平仓本金{closePrincipal:F2}(疑似interestWindowEmpty种子未重放/日终归档缺失," +
$"请核对swap_flow_event.InterestPrincipal重放回写与eod_swap_position归档)");
}
// 不得超过实结金额(数据异常时钳制并留痕,避免负进入计息)
// 已并复利本金不得超过实结金额(数据异常时钳制并留痕,避免负的段内已计利息进入计息)
if (capitalized > Math.Max(0m, normalEvent.InterestAmount))
{
trace?.Note($"PENALTY|融资腿{position.id} 注意 承接钳制:推导 {capitalized:F4} > 实结 {normalEvent.InterestAmount:F4}(快照/事件数据异常,请核对 preEod.TdInterestPrincipal/InterestIncomeSum");
trace?.Note($"PENALTY|融资腿{position.id} 注意 承接已并复利本金钳制:推导 {capitalized:F4} > 实结 {normalEvent.InterestAmount:F4}(快照/事件数据异常,请核对 preEod.TdInterestPrincipal/InterestIncomeSum");
capitalized = Math.Max(0m, normalEvent.InterestAmount);
}
return (capitalized, normalEvent.InterestAmount - capitalized);
+12 -12
View File
@@ -905,13 +905,13 @@ namespace YLErp.Modules.SwapModule
isResetDay ? endDate : startDate, position.interest_rule);
// 历史上有"取错重置日利率"的线上 bug,取价决策必须常驻落盘(SwapCalcTrace.Critical 无条件 Info)。
SwapCalcTrace.Critical(
$"FIX GetFloatRate p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] days={days} period={period} " +
$"FIX GetFloatRate 融资腿{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] days={days} period={period} " +
$"重置日={isResetDay} rule={position.interest_rule} 取价日={rateDate:yyyy-MM-dd} calcLast={calcLast} " +
$"preEod={(preEod.id != 0 ? $"{preEod.ValueDate:yyyy-MM-dd}:{preEod.FloatRate:P6}" : "")}");
if (preEod.id != 0 && !isResetDay)
{
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 非重置日→沿用昨日终FloatRate={preEod.FloatRate:P6}");
SwapCalcTrace.Critical($"FIX GetFloatRate 融资腿{position.id} 非重置日→沿用昨日终FloatRate={preEod.FloatRate:P6}");
position.FloatRate = positionClone.FloatRate = preEod.FloatRate;
return preEod.FloatRate;
}
@@ -923,13 +923,13 @@ namespace YLErp.Modules.SwapModule
{
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")})");
$"FIX GetFloatRate 融资腿{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}");
SwapCalcTrace.Critical($"FIX GetFloatRate 融资腿{position.id} 重置日→取{rateDate:yyyy-MM-dd}定盘={rate:P6}");
position.FloatRate = positionClone.FloatRate = rate;
return position.FloatRate;
}
@@ -937,17 +937,17 @@ namespace YLErp.Modules.SwapModule
{
if (calcLast)
{
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且算尾→抛异常拦截");
SwapCalcTrace.Critical($"FIX GetFloatRate 融资腿{position.id} {rateDate:yyyy-MM-dd}缺价且算尾→抛异常拦截");
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
}
// 算头不算尾(calcLast=false)endDate 当天不计息,其 FR007 利率不参与计息,
// 缺价时直接沿用已有利率,不回退取其他日期利率,不告警。
var kept = preEod.id != 0 ? preEod.FloatRate : position.FloatRate;
SwapCalcTrace.Critical(
$"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且不算尾→沿用已有利率={kept:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})");
$"FIX GetFloatRate 融资腿{position.id} {rateDate:yyyy-MM-dd}缺价且不算尾→沿用已有利率={kept:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})");
return kept;
}
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 计息窗口为空→利率不参与,返回0");
SwapCalcTrace.Critical($"FIX GetFloatRate 融资腿{position.id} 计息窗口为空→利率不参与,返回0");
return 0m;
}
@@ -1376,15 +1376,15 @@ namespace YLErp.Modules.SwapModule
if (fixing != 0m)
{
SwapCalcTrace.Critical(
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→定盘={fixing:P6}");
$"FIX Resolve 融资腿{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→定盘={fixing:P6}");
return fixing;
}
SwapCalcTrace.Critical(
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd})→定盘=0视为缺价,沿用fallback={fallback:P6}");
$"FIX Resolve 融资腿{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd})→定盘=0视为缺价,沿用fallback={fallback:P6}");
return fallback;
}
SwapCalcTrace.Critical(
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→缺价,抛异常");
$"FIX Resolve 融资腿{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→缺价,抛异常");
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
@@ -1403,7 +1403,7 @@ namespace YLErp.Modules.SwapModule
var calcDays = (endDate - startDate).Days;
decimal currentFloat = initialFloat;
SwapCalcTrace.Critical(
$"FIX Segments p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] period={interestPeriod} " +
$"FIX Segments 融资腿{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] period={interestPeriod} " +
$"fetchAfter={(fetchAfterDate?.ToString("yyyy-MM-dd") ?? "")} calcLast={calcLast} " +
$"排除起点={(exclusionStart?.ToString("yyyy-MM-dd") ?? (calcLast ? "" : endDate.ToString("yyyy-MM-dd")))} seed={initialFloat:P6} spread={spread:P6}");
for (int i = 0; i <= calcDays; i += interestPeriod)
@@ -1422,7 +1422,7 @@ namespace YLErp.Modules.SwapModule
else if (needFetch && isExcludedEnd)
{
SwapCalcTrace.Critical(
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日(不计息)→不取价,沿用末段={currentFloat:P6}");
$"FIX Segment 融资腿{position.id} {resetDate:yyyy-MM-dd} 排除日(不计息)→不取价,沿用末段={currentFloat:P6}");
}
rates.Add((resetDate, spread + currentFloat));
}
@@ -1380,7 +1380,7 @@ namespace YLErp.Modules.SwapModule
{
var ongoingFixing = ResolveOngoingResetFixing(position, valueDate);
SwapCalcTrace.Critical(
$"FIX EodCloseRefix p{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}");
$"FIX EodCloseRefix 融资腿{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}");
newEodPayPosition.FloatRate = ongoingFixing;
}
//利息端估值用信息